From cbdfa516cea90f541314166210153c950012d66b Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Thu, 14 Sep 2023 21:55:26 +0200 Subject: [PATCH 01/21] Add discussion class with data scraper --- classes/CSteamDiscussion.js | 108 ++++++++++++++++++++++++++++++++++++ index.js | 1 + 2 files changed, 109 insertions(+) create mode 100644 classes/CSteamDiscussion.js diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js new file mode 100644 index 0000000..f44fb07 --- /dev/null +++ b/classes/CSteamDiscussion.js @@ -0,0 +1,108 @@ +const Cheerio = require('cheerio'); +const SteamID = require('steamid'); + +const SteamCommunity = require('../index.js'); +const Helpers = require('../components/helpers.js'); + + +/** + * Scrape a discussion's DOM to get all available information + * @param {string} url - SteamCommunity url pointing to the discussion to fetch + * @param {function} callback - First argument is null/Error, second is object containing all available information + */ +SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { + // Construct object holding all the data we can scrape + let discussion = { + id: null, + appID: null, + forumID: null, + author: null, + postedDate: null, + title: null, + content: null, + commentsAmount: null // I originally wanted to fetch all comments by default but that would have been a lot of potentially unused data + }; + + // Get DOM of discussion + this.httpRequestGet(url, (err, res, body) => { + try { + + /* --------------------- Preprocess output --------------------- */ + + // Load output into cheerio to make parsing easier + let $ = Cheerio.load(body); + + // Get breadcrumbs once + let breadcrumbs = $(".forum_breadcrumbs").children(); + + + /* --------------------- Find and map values --------------------- */ + + // Get discussionID from url + discussion.id = url.split("/")[url.split("/").length - 1]; + + + // Get appID from breadcrumbs + let appIdHref = breadcrumbs[0].attribs["href"].split("/"); + + discussion.appID = appIdHref[appIdHref.length - 1]; + + + // Get forumID from breadcrumbs + let forumIdHref = breadcrumbs[2].attribs["href"].split("/"); + + discussion.forumID = forumIdHref[forumIdHref.length - 2]; + + + // Find postedDate and convert to timestamp + let posted = $(".topicstats > .topicstats_label:contains(\"Date Posted:\")").next().text() + + discussion.postedDate = Helpers.decodeSteamTime(posted.trim()); + + + // Find commentsAmount + discussion.commentsAmount = Number($(".topicstats > .topicstats_label:contains(\"Posts:\")").next().text()); + + + // Get discussion title & content + discussion.title = $(".forum_op > .topic").text().trim(); + discussion.content = $(".forum_op > .content").text().trim(); + + + // Find author and convert to SteamID object + let authorLink = $(".authorline > .forum_op_author").attr("href"); + + Helpers.resolveVanityURL(authorLink, (err, data) => { // This request takes <1 sec + if (err) { + callback(err); + return; + } + + discussion.author = new SteamID(data.steamID); + + // Make callback when ID was resolved as otherwise owner will always be null + callback(null, new CSteamDiscussion(this, discussion)); + }); + + } catch (err) { + callback(err, null); + } + }, "steamcommunity"); +} + + +/** + * Constructor - Creates a new Discussion object + * @class + * @param {SteamCommunity} community + * @param {{ id: string, appID: string, forumID: string, author: SteamID, postedDate: Object, title: string, content: string, commentsAmount: number }} data + */ +function CSteamDiscussion(community, data) { + /** + * @type {SteamCommunity} + */ + this._community = community; + + // Clone all the data we received + Object.assign(this, data); +} \ No newline at end of file diff --git a/index.js b/index.js index 0fc31ed..60fd8c7 100644 --- a/index.js +++ b/index.js @@ -590,6 +590,7 @@ require('./components/confirmations.js'); require('./components/help.js'); require('./classes/CMarketItem.js'); require('./classes/CMarketSearchResult.js'); +require('./classes/CSteamDiscussion.js'); require('./classes/CSteamGroup.js'); require('./classes/CSteamSharedFile.js'); require('./classes/CSteamUser.js'); From 5cce2e776682a6dafd78f9f33780a0175b607020 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Fri, 15 Sep 2023 10:44:35 +0200 Subject: [PATCH 02/21] Scrape comment id marked as answer --- classes/CSteamDiscussion.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index f44fb07..56543a9 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -20,7 +20,8 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { postedDate: null, title: null, content: null, - commentsAmount: null // I originally wanted to fetch all comments by default but that would have been a lot of potentially unused data + commentsAmount: null, // I originally wanted to fetch all comments by default but that would have been a lot of potentially unused data + answerCommentIndex: null }; // Get DOM of discussion @@ -69,6 +70,17 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { discussion.content = $(".forum_op > .content").text().trim(); + // Find comment marked as answer + let hasAnswer = $(".commentthread_answer_bar") + + if (hasAnswer.length != 0) { + let answerPermLink = hasAnswer.next().children(".forum_comment_permlink").text().trim(); + + // Convert comment id to number, remove hashtag and subtract by 1 to make it an index + discussion.answerCommentIndex = Number(answerPermLink.replace("#", "")) - 1; + } + + // Find author and convert to SteamID object let authorLink = $(".authorline > .forum_op_author").attr("href"); From ab658f240acf18ab6d40ef53cdbc4ab8f5913585 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sat, 16 Sep 2023 00:47:07 +0200 Subject: [PATCH 03/21] Add discussion comment scraping support --- components/discussions.js | 119 ++++++++++++++++++++++++++++++++++++++ index.js | 1 + 2 files changed, 120 insertions(+) create mode 100644 components/discussions.js diff --git a/components/discussions.js b/components/discussions.js new file mode 100644 index 0000000..6d60e2c --- /dev/null +++ b/components/discussions.js @@ -0,0 +1,119 @@ +const Cheerio = require('cheerio'); + +const SteamCommunity = require('../index.js'); +const Helpers = require('../components/helpers.js'); + + +/** + * Scrapes a range of comments from a Steam discussion + * @param {url} url - SteamCommunity url pointing to the discussion to fetch + * @param {number} startIndex - Index (0 based) of the first comment to fetch + * @param {number} endIndex - Index (0 based) of the last comment to fetch + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIndex, callback) { + this.httpRequestGet(url + "?l=en", async (err, res, body) => { + + if (err) { + callback("Failed to load discussion: " + err, null); + return; + } + + + // Load output into cheerio to make parsing easier + let $ = Cheerio.load(body); + + let paging = $(".forum_paging > .forum_paging_summary").children(); + + /** + * Stores every loaded page inside a Cheerio instance + * @type {{[key: number]: cheerio.Root}} + */ + let pages = { + 0: $ + }; + + + // Determine amount of comments per page and total. Update endIndex if null to get all comments + let commentsPerPage = Number(paging[4].children[0].data); + let totalComments = Number(paging[5].children[0].data) + + if (endIndex == null || endIndex > totalComments - 1) { // Make sure to check against null as the index 0 would cast to false + endIndex = totalComments - 1; + } + + + // Save all pages that need to be fetched in order to get the requested comments + let firstPage = Math.trunc(startIndex / commentsPerPage); // Index of the first page that needs to be fetched + let lastPage = Math.trunc(endIndex / commentsPerPage); + let promises = []; + + for (let i = firstPage; i <= lastPage; i++) { + if (i == 0) continue; // First page is already in pages object + + promises.push(new Promise((resolve) => { + setTimeout(() => { // Delay fetching a bit to reduce the risk of Steam blocking us + + this.httpRequestGet(url + "?l=en&ctp=" + (i + 1), (err, res, body) => { + try { + pages[i] = Cheerio.load(body); + resolve(); + } catch (err) { + return callback("Failed to load comments page: " + err, null); + } + }, "steamcommunity"); + + }, 250 * i); + })); + } + + await Promise.all(promises); // Wait for all pages to be fetched + + + // Fill comments with content of all comments + let comments = []; + + for (let i = startIndex; i <= endIndex; i++) { + let $ = pages[Math.trunc(i / commentsPerPage)]; + + let thisComment = $(`.forum_comment_permlink:contains("#${i + 1}")`).parent(); + + // Note: '>' inside the cheerio selectors didn't work here + let authorContainer = thisComment.children(".commentthread_comment_content").children(".commentthread_comment_author").children(".commentthread_author_link"); + let commentContainer = thisComment.children(".commentthread_comment_content").children(".commentthread_comment_text"); + + + // Prepare comment text + let commentText = ""; + + if (commentContainer.children(".bb_blockquote").length != 0) { // Check if comment contains quote + commentText += commentContainer.children(".bb_blockquote").children(".bb_quoteauthor").text() + "\n"; // Get quote header and add a proper newline + + let quoteWithNewlines = commentContainer.children(".bb_blockquote").first().find("br").replaceWith("\n"); // Replace
's with newlines to get a proper output + + commentText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text' }).text().trim(); // Get blockquote content without child content - https://stackoverflow.com/a/23956052 + + commentText += "\n\n-------\n\n"; // Add spacer + } + + let quoteWithNewlines = commentContainer.first().find("br").replaceWith("\n"); // Replace
's with newlines to get a proper output + + commentText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text' }).text().trim(); // Add comment content without child content - https://stackoverflow.com/a/23956052 + + + comments.push({ + index: i, + commentId: thisComment.attr("id").replace("comment_", ""), + commentLink: `${url}#${thisComment.attr("id").replace("comment_", "c")}`, + authorLink: authorContainer.attr("href"), // I did not call 'resolveVanityURL()' here and convert to SteamID to reduce the amount of potentially unused Steam pings + postedDate: Helpers.decodeSteamTime(authorContainer.children(".commentthread_comment_timestamp").text().trim()), + content: commentText.trim() + }); + } + + + // Callback our result + callback(null, comments); + + }, "steamcommunity"); +}; \ No newline at end of file diff --git a/index.js b/index.js index 60fd8c7..e8ce722 100644 --- a/index.js +++ b/index.js @@ -587,6 +587,7 @@ require('./components/inventoryhistory.js'); require('./components/webapi.js'); require('./components/twofactor.js'); require('./components/confirmations.js'); +require('./components/discussions.js'); require('./components/help.js'); require('./classes/CMarketItem.js'); require('./classes/CMarketSearchResult.js'); From bdb3b5b427f7786310bf935575da4b30670ad643 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sat, 16 Sep 2023 23:10:18 +0200 Subject: [PATCH 04/21] Add discussion subscribing support --- components/discussions.js | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/components/discussions.js b/components/discussions.js index 6d60e2c..6a0941f 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -116,4 +116,54 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn callback(null, comments); }, "steamcommunity"); +}; + +/** + * Subscribes to a discussion's comment section + * @param {String} topicOwner - ID of the topic owner + * @param {String} gidforum - GID of the discussion's forum + * @param {String} discussionId - ID of the discussion + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidforum, discussionId, callback) { + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/ForumTopic/subscribe/${topicOwner}/${gidforum}/`, + "form": { + "count": 15, + "sessionid": this.getSessionID(), + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1,"is_banned":0,"can_delete":0,"can_edit":0}}', + "feature2": discussionId + } + }, function(err, response, body) { // eslint-disable-line + if (!callback) { + return; + } + + callback(err); + }, "steamcommunity"); +}; + +/** + * Unsubscribes from a discussion's comment section + * @param {String} topicOwner - ID of the topic owner + * @param {String} gidforum - GID of the discussion's forum + * @param {String} discussionId - ID of the discussion + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gidforum, discussionId, callback) { + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/ForumTopic/unsubscribe/${topicOwner}/${gidforum}/`, + "form": { + "count": 15, + "sessionid": this.getSessionID(), + "extended_data": '{}', // Unsubscribing does not require any data here + "feature2": discussionId + } + }, function(err, response, body) { // eslint-disable-line + if (!callback) { + return; + } + + callback(err); + }, "steamcommunity"); }; \ No newline at end of file From 80755d85921940b0e64a8071d8da76190f1d8f9c Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sat, 16 Sep 2023 23:23:37 +0200 Subject: [PATCH 05/21] Add discussion commenting support --- components/discussions.js | 56 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/components/discussions.js b/components/discussions.js index 6a0941f..25d23c1 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -118,6 +118,60 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn }, "steamcommunity"); }; +/** + * Posts a comment to a discussion + * @param {String} topicOwner - ID of the topic owner + * @param {String} gidforum - GID of the discussion's forum + * @param {String} discussionId - ID of the discussion + * @param {String} message - Content of the comment to post + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, discussionId, message, callback) { + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/ForumTopic/post/${topicOwner}/${gidforum}/`, + "form": { + "comment": message, + "count": 15, + "sessionid": this.getSessionID(), + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1,"is_banned":0,"can_delete":0,"can_edit":0}}', // This parameter is required, not sure about the specific settings + "feature2": discussionId + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(err); + }, "steamcommunity"); +}; + +/** + * Deletes a comment from a discussion + * @param {String} topicOwner - ID of the topic owner + * @param {String} gidforum - GID of the discussion's forum + * @param {String} discussionId - ID of the discussion + * @param {String} gidcomment - ID of the comment to delete + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum, discussionId, gidcomment, callback) { + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/ForumTopic/delete/${topicOwner}/${gidforum}/`, + "form": { + "gidcomment": gidcomment, + "count": 15, + "sessionid": this.getSessionID(), + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1,"is_banned":0,"can_delete":0,"can_edit":0}}', // This parameter is required, not sure about the specific settings + "feature2": discussionId + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(err); + }, "steamcommunity"); +}; + /** * Subscribes to a discussion's comment section * @param {String} topicOwner - ID of the topic owner @@ -131,7 +185,7 @@ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidf "form": { "count": 15, "sessionid": this.getSessionID(), - "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1,"is_banned":0,"can_delete":0,"can_edit":0}}', + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1,"is_banned":0,"can_delete":0,"can_edit":0}}', // This parameter is required, not sure about the specific settings "feature2": discussionId } }, function(err, response, body) { // eslint-disable-line From 3b8f92e2b682e2639d3c983a19c6fffe1962957f Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sat, 16 Sep 2023 23:53:44 +0200 Subject: [PATCH 06/21] Scrape gidforum & topicOwner --- classes/CSteamDiscussion.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index 56543a9..4e7948e 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -1,7 +1,7 @@ const Cheerio = require('cheerio'); const SteamID = require('steamid'); -const SteamCommunity = require('../index.js'); +const SteamCommunity = require('../index.js'); const Helpers = require('../components/helpers.js'); @@ -16,6 +16,8 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { id: null, appID: null, forumID: null, + gidforum: null, // This is some id used as parameter 2 in post requests + topicOwner: null, // This is some id used as parameter 1 in post requests author: null, postedDate: null, title: null, @@ -55,6 +57,13 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { discussion.forumID = forumIdHref[forumIdHref.length - 2]; + // Get gidforum and topicOwner. I'm not 100% sure what they are, they are however used for all post requests + let gids = $(".forum_paging > .forum_paging_controls").attr("id").split("_"); + + discussion.gidforum = gids[3]; + discussion.topicOwner = gids[2]; + + // Find postedDate and convert to timestamp let posted = $(".topicstats > .topicstats_label:contains(\"Date Posted:\")").next().text() From 5c48d7b746dd6d6c839c75a7fe12f2e55be2a343 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sun, 17 Sep 2023 00:18:11 +0200 Subject: [PATCH 07/21] Add discussion object methods --- classes/CSteamDiscussion.js | 40 ++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index 4e7948e..f59997e 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -126,4 +126,42 @@ function CSteamDiscussion(community, data) { // Clone all the data we received Object.assign(this, data); -} \ No newline at end of file +} + + +/** + * Posts a comment to this discussion's comment section + * @param {String} message - Content of the comment to post + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamDiscussion.prototype.postComment = function(message, callback) { + this._community.postDiscussionComment(this.topicOwner, this.gidforum, this.id, message, callback); +}; + + +/** + * Delete a comment from this discussion's comment section + * @param {String} gidcomment - ID of the comment to delete + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamDiscussion.prototype.deleteComment = function(gidcomment, callback) { + this._community.deleteDiscussionComment(this.topicOwner, this.gidforum, this.id, gidcomment, callback); +}; + + +/** + * Subscribes to this discussion's comment section + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamDiscussion.prototype.subscribe = function(callback) { + this._community.subscribeDiscussionComments(this.topicOwner, this.gidforum, this.id, callback); +}; + + +/** + * Unsubscribes from this discussion's comment section + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamDiscussion.prototype.unsubscribe = function(callback) { + this._community.unsubscribeDiscussionComments(this.topicOwner, this.gidforum, this.id, callback); +}; From 660c9151f7ca9d8883b3d851d8737f5e5c4d3b0c Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sun, 17 Sep 2023 00:18:40 +0200 Subject: [PATCH 08/21] Get id more reliably and misc --- classes/CSteamDiscussion.js | 7 ++----- components/discussions.js | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index f59997e..35072e1 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -41,10 +41,6 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { /* --------------------- Find and map values --------------------- */ - // Get discussionID from url - discussion.id = url.split("/")[url.split("/").length - 1]; - - // Get appID from breadcrumbs let appIdHref = breadcrumbs[0].attribs["href"].split("/"); @@ -57,9 +53,10 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { discussion.forumID = forumIdHref[forumIdHref.length - 2]; - // Get gidforum and topicOwner. I'm not 100% sure what they are, they are however used for all post requests + // Get id, gidforum and topicOwner. The first is used in the URL itself, the other two only in post requests let gids = $(".forum_paging > .forum_paging_controls").attr("id").split("_"); + discussion.id = gids[4]; discussion.gidforum = gids[3]; discussion.topicOwner = gids[2]; diff --git a/components/discussions.js b/components/discussions.js index 25d23c1..3bf2108 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -133,7 +133,7 @@ SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, "comment": message, "count": 15, "sessionid": this.getSessionID(), - "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1,"is_banned":0,"can_delete":0,"can_edit":0}}', // This parameter is required, not sure about the specific settings + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', "feature2": discussionId } }, function(err, response, body) { @@ -160,7 +160,7 @@ SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum "gidcomment": gidcomment, "count": 15, "sessionid": this.getSessionID(), - "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1,"is_banned":0,"can_delete":0,"can_edit":0}}', // This parameter is required, not sure about the specific settings + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', "feature2": discussionId } }, function(err, response, body) { @@ -185,7 +185,7 @@ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidf "form": { "count": 15, "sessionid": this.getSessionID(), - "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1,"is_banned":0,"can_delete":0,"can_edit":0}}', // This parameter is required, not sure about the specific settings + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', "feature2": discussionId } }, function(err, response, body) { // eslint-disable-line From d5f9dc762234aebfb6c8faec3afb431db7dcd882 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sun, 17 Sep 2023 00:26:26 +0200 Subject: [PATCH 09/21] Add missing getComments() object method and fix callback jsdoc --- classes/CSteamDiscussion.js | 11 +++++++++++ components/discussions.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index 35072e1..4461322 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -126,6 +126,17 @@ function CSteamDiscussion(community, data) { } +/** + * Scrapes a range of comments from this discussion + * @param {number} startIndex - Index (0 based) of the first comment to fetch + * @param {number} endIndex - Index (0 based) of the last comment to fetch + * @param {function} callback - First argument is null/Error, second is array containing the requested comments + */ +CSteamDiscussion.prototype.getComments = function(startIndex, endIndex, callback) { + this._community.getDiscussionComments(`https://steamcommunity.com/app/${this.appID}/discussions/${this.forumID}/${this.id}`, startIndex, endIndex, callback); +}; + + /** * Posts a comment to this discussion's comment section * @param {String} message - Content of the comment to post diff --git a/components/discussions.js b/components/discussions.js index 3bf2108..146dc8a 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -9,7 +9,7 @@ const Helpers = require('../components/helpers.js'); * @param {url} url - SteamCommunity url pointing to the discussion to fetch * @param {number} startIndex - Index (0 based) of the first comment to fetch * @param {number} endIndex - Index (0 based) of the last comment to fetch - * @param {function} callback - Takes only an Error object/null as the first argument + * @param {function} callback - First argument is null/Error, second is array containing the requested comments */ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIndex, callback) { this.httpRequestGet(url + "?l=en", async (err, res, body) => { From ee4c59f455d7087a8007a46c27e25eff996fc4b6 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sun, 17 Sep 2023 00:56:00 +0200 Subject: [PATCH 10/21] Fix first comment getting blockquote from another comment --- components/discussions.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/components/discussions.js b/components/discussions.js index 146dc8a..12d0c14 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -77,13 +77,14 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn let $ = pages[Math.trunc(i / commentsPerPage)]; let thisComment = $(`.forum_comment_permlink:contains("#${i + 1}")`).parent(); + let thisCommentID = thisComment.attr("id").replace("comment_", ""); // Note: '>' inside the cheerio selectors didn't work here - let authorContainer = thisComment.children(".commentthread_comment_content").children(".commentthread_comment_author").children(".commentthread_author_link"); - let commentContainer = thisComment.children(".commentthread_comment_content").children(".commentthread_comment_text"); + let authorContainer = thisComment.children(".commentthread_comment_content").children(".commentthread_comment_author").children(".commentthread_author_link"); + let commentContainer = thisComment.children(".commentthread_comment_content").children(`#comment_content_${thisCommentID}`); - // Prepare comment text + // Prepare comment text by formatting the blockquote if one exists first, then adding the actual content let commentText = ""; if (commentContainer.children(".bb_blockquote").length != 0) { // Check if comment contains quote @@ -103,8 +104,8 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn comments.push({ index: i, - commentId: thisComment.attr("id").replace("comment_", ""), - commentLink: `${url}#${thisComment.attr("id").replace("comment_", "c")}`, + commentId: thisCommentID, + commentLink: `${url}#c${thisCommentID}`, authorLink: authorContainer.attr("href"), // I did not call 'resolveVanityURL()' here and convert to SteamID to reduce the amount of potentially unused Steam pings postedDate: Helpers.decodeSteamTime(authorContainer.children(".commentthread_comment_timestamp").text().trim()), content: commentText.trim() From fd8a03bd38bfac24c8bff3907e8acf3a9c2bc7e5 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sun, 17 Sep 2023 22:05:34 +0200 Subject: [PATCH 11/21] Add type detection and support forum & group discussions --- classes/CSteamDiscussion.js | 29 ++++++++++++++++++++++++----- resources/EDiscussionType.js | 13 +++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 resources/EDiscussionType.js diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index 4461322..f0e297c 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -4,6 +4,8 @@ const SteamID = require('steamid'); const SteamCommunity = require('../index.js'); const Helpers = require('../components/helpers.js'); +const EDiscussionType = require("../resources/EDiscussionType.js"); + /** * Scrape a discussion's DOM to get all available information @@ -14,6 +16,7 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { // Construct object holding all the data we can scrape let discussion = { id: null, + type: null, appID: null, forumID: null, gidforum: null, // This is some id used as parameter 2 in post requests @@ -38,17 +41,33 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { // Get breadcrumbs once let breadcrumbs = $(".forum_breadcrumbs").children(); + if (breadcrumbs.length == 0) breadcrumbs = $(".group_breadcrumbs").children(); + /* --------------------- Find and map values --------------------- */ - // Get appID from breadcrumbs - let appIdHref = breadcrumbs[0].attribs["href"].split("/"); + // Determine type from URL as some checks will deviate, depending on the type + if (url.includes("steamcommunity.com/discussions/forum")) discussion.type = EDiscussionType.Forum; + if (/steamcommunity.com\/app\/.+\/discussions/g.test(url)) discussion.type = EDiscussionType.App; + if (/steamcommunity.com\/groups\/.+\/discussions/g.test(url)) discussion.type = EDiscussionType.Group; - discussion.appID = appIdHref[appIdHref.length - 1]; + + // Get appID from breadcrumbs if this discussion is associated to one + if (discussion.type == EDiscussionType.App) { + let appIdHref = breadcrumbs[0].attribs["href"].split("/"); + + discussion.appID = appIdHref[appIdHref.length - 1]; + } // Get forumID from breadcrumbs - let forumIdHref = breadcrumbs[2].attribs["href"].split("/"); + let forumIdHref; + + if (discussion.type == EDiscussionType.Group) { // Groups have an extra breadcrumb so we need to shift by 2 + forumIdHref = breadcrumbs[4].attribs["href"].split("/"); + } else { + forumIdHref = breadcrumbs[2].attribs["href"].split("/"); + } discussion.forumID = forumIdHref[forumIdHref.length - 2]; @@ -62,7 +81,7 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { // Find postedDate and convert to timestamp - let posted = $(".topicstats > .topicstats_label:contains(\"Date Posted:\")").next().text() + let posted = $(".topicstats > .topicstats_label:contains(\"Date Posted:\")").next().text(); discussion.postedDate = Helpers.decodeSteamTime(posted.trim()); diff --git a/resources/EDiscussionType.js b/resources/EDiscussionType.js new file mode 100644 index 0000000..b6c28bb --- /dev/null +++ b/resources/EDiscussionType.js @@ -0,0 +1,13 @@ +/** + * @enum EDiscussionType + */ +module.exports = { + "Forum": 0, + "App": 1, + "Group": 2, + + // Value-to-name mapping for convenience + "0": "Forum", + "1": "App", + "2": "Group" +}; \ No newline at end of file From 4e9155c8989f535d3b1cd51bc3c601e8a4343cbc Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sun, 17 Sep 2023 22:48:38 +0200 Subject: [PATCH 12/21] Add setDiscussionCommentsPerPage() --- components/discussions.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/components/discussions.js b/components/discussions.js index 12d0c14..afd5290 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -219,6 +219,30 @@ SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gi return; } + callback(err); + }, "steamcommunity"); +}; + +/** + * Sets an amount of comments per page + * @param {String} value - 15, 30 or 50 + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.setDiscussionCommentsPerPage = function(value, callback) { + if (!["15", "30", "50"].includes(value)) value = "50"; // Check for invalid setting + + this.httpRequestPost({ + "uri": `https://steamcommunity.com/forum/0/0/setpreference`, + "form": { + "preference": "topicrepliesperpage", + "value": value, + "sessionid": this.getSessionID(), + } + }, function(err, response, body) { // eslint-disable-line + if (!callback) { + return; + } + callback(err); }, "steamcommunity"); }; \ No newline at end of file From a5a3c8edb5165b838f68b2a5fa821ddec26571a2 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:07:31 +0200 Subject: [PATCH 13/21] Add support for displaying multiple nested quotes --- classes/CSteamDiscussion.js | 2 +- components/discussions.js | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index f0e297c..a1dd872 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -38,7 +38,7 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { // Load output into cheerio to make parsing easier let $ = Cheerio.load(body); - // Get breadcrumbs once + // Get breadcrumbs once. Depending on the type of discussion, it either uses "forum" or "group" breadcrumbs let breadcrumbs = $(".forum_breadcrumbs").children(); if (breadcrumbs.length == 0) breadcrumbs = $(".group_breadcrumbs").children(); diff --git a/components/discussions.js b/components/discussions.js index afd5290..7fed346 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -84,17 +84,33 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn let commentContainer = thisComment.children(".commentthread_comment_content").children(`#comment_content_${thisCommentID}`); - // Prepare comment text by formatting the blockquote if one exists first, then adding the actual content + // Prepare comment text by finding all existing blockquotes, formatting them and adding them infront each other. Afterwards handle the text itself let commentText = ""; + let blockQuoteSelector = ".bb_blockquote"; + let children = commentContainer.children(blockQuoteSelector); - if (commentContainer.children(".bb_blockquote").length != 0) { // Check if comment contains quote - commentText += commentContainer.children(".bb_blockquote").children(".bb_quoteauthor").text() + "\n"; // Get quote header and add a proper newline + for (let i = 0; i < 10; i++) { // I'm not sure how I could dynamically check the amount of nested blockquotes. 10 is prob already too much to stay readable + if (children.length > 0) { + let thisQuoteText = ""; - let quoteWithNewlines = commentContainer.children(".bb_blockquote").first().find("br").replaceWith("\n"); // Replace
's with newlines to get a proper output + thisQuoteText += children.children(".bb_quoteauthor").text() + "\n"; // Get quote header and add a proper newline - commentText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text' }).text().trim(); // Get blockquote content without child content - https://stackoverflow.com/a/23956052 + // Replace
's with newlines to get a proper output + let quoteWithNewlines = children.first().find("br").replaceWith("\n"); - commentText += "\n\n-------\n\n"; // Add spacer + thisQuoteText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text' }).text().trim(); // Get blockquote content without child content - https://stackoverflow.com/a/23956052 + if (i > 0) thisQuoteText += "\n-------\n"; // Add spacer + + commentText = thisQuoteText + commentText; // Concat quoteText to the start of commentText as the most nested quote is the first one inside the comment chain itself + + // Go one level deeper + children = children.children(blockQuoteSelector); + + } else { + + commentText += "\n\n-------\n\n"; // Add spacer + break; + } } let quoteWithNewlines = commentContainer.first().find("br").replaceWith("\n"); // Replace
's with newlines to get a proper output From eddc1d12751711e3ae8fd45eb751e4a0fd106ef9 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:58:58 +0200 Subject: [PATCH 14/21] Fix checking for a rejected request --- components/discussions.js | 99 ++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 18 deletions(-) diff --git a/components/discussions.js b/components/discussions.js index 7fed346..fe3bcb9 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -151,14 +151,25 @@ SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, "count": 15, "sessionid": this.getSessionID(), "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - "feature2": discussionId - } + "feature2": discussionId, + "json": 1 + }, + "json": true }, function(err, response, body) { if (!callback) { return; } - callback(err); + if (err) { + callback(err); + return; + } + + if (body.success) { + callback(null); + } else { + callback(new Error(body.error)); + } }, "steamcommunity"); }; @@ -178,14 +189,25 @@ SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum "count": 15, "sessionid": this.getSessionID(), "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - "feature2": discussionId - } - }, function(err, response, body) { + "feature2": discussionId, + "json": 1 + }, + "json": true + }, function(err, response, body) { // Steam does not seem to return any errors here even when trying to delete a non-existing comment but let's check the response anyway if (!callback) { return; } - callback(err); + if (err) { + callback(err); + return; + } + + if (body.success) { + callback(null); + } else { + callback(new Error(body.error)); + } }, "steamcommunity"); }; @@ -203,14 +225,28 @@ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidf "count": 15, "sessionid": this.getSessionID(), "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - "feature2": discussionId - } - }, function(err, response, body) { // eslint-disable-line + "feature2": discussionId, + "json": 1 + }, + "json": true + }, function(err, response, body) { if (!callback) { return; } - callback(err); + if (err) { + callback(err); + return; + } + + if (body.success && body.success != SteamCommunity.EResult.OK) { + let err = new Error(body.message || SteamCommunity.EResult[body.success]); + err.eresult = err.code = body.success; + callback(err); + return; + } + + callback(null); }, "steamcommunity"); }; @@ -228,14 +264,28 @@ SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gi "count": 15, "sessionid": this.getSessionID(), "extended_data": '{}', // Unsubscribing does not require any data here - "feature2": discussionId - } - }, function(err, response, body) { // eslint-disable-line + "feature2": discussionId, + "json": 1 + }, + "json": true + }, function(err, response, body) { if (!callback) { return; } - callback(err); + if (err) { + callback(err); + return; + } + + if (body.success && body.success != SteamCommunity.EResult.OK) { + let err = new Error(body.message || SteamCommunity.EResult[body.success]); + err.eresult = err.code = body.success; + callback(err); + return; + } + + callback(null); }, "steamcommunity"); }; @@ -253,12 +303,25 @@ SteamCommunity.prototype.setDiscussionCommentsPerPage = function(value, callback "preference": "topicrepliesperpage", "value": value, "sessionid": this.getSessionID(), - } - }, function(err, response, body) { // eslint-disable-line + }, + "json": true + }, function(err, response, body) { // Steam does not seem to return any errors for this request if (!callback) { return; } - callback(err); + if (err) { + callback(err); + return; + } + + if (body.success && body.success != SteamCommunity.EResult.OK) { + let err = new Error(body.message || SteamCommunity.EResult[body.success]); + err.eresult = err.code = body.success; + callback(err); + return; + } + + callback(null); }, "steamcommunity"); }; \ No newline at end of file From a4eb96452d1edcdd69ba04372337bb36004ffbc9 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sun, 1 Oct 2023 22:26:51 +0200 Subject: [PATCH 15/21] Use eresultError() helper --- components/discussions.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/components/discussions.js b/components/discussions.js index fe3bcb9..a892c06 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -1,7 +1,7 @@ const Cheerio = require('cheerio'); const SteamCommunity = require('../index.js'); -const Helpers = require('../components/helpers.js'); +const Helpers = require('./helpers.js'); /** @@ -240,9 +240,7 @@ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidf } if (body.success && body.success != SteamCommunity.EResult.OK) { - let err = new Error(body.message || SteamCommunity.EResult[body.success]); - err.eresult = err.code = body.success; - callback(err); + callback(Helpers.eresultError(body.success)); return; } @@ -279,9 +277,7 @@ SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gi } if (body.success && body.success != SteamCommunity.EResult.OK) { - let err = new Error(body.message || SteamCommunity.EResult[body.success]); - err.eresult = err.code = body.success; - callback(err); + callback(Helpers.eresultError(body.success)); return; } @@ -316,9 +312,7 @@ SteamCommunity.prototype.setDiscussionCommentsPerPage = function(value, callback } if (body.success && body.success != SteamCommunity.EResult.OK) { - let err = new Error(body.message || SteamCommunity.EResult[body.success]); - err.eresult = err.code = body.success; - callback(err); + callback(Helpers.eresultError(body.success)); return; } From d46b7c88042b5530f3e5ae00084e1dd07545a4d9 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:01:12 +0200 Subject: [PATCH 16/21] Buff eslint's happiness by 80% --- classes/CSteamDiscussion.js | 36 ++++----- components/discussions.js | 142 +++++++++++++++++------------------ resources/EDiscussionType.js | 12 +-- 3 files changed, 95 insertions(+), 95 deletions(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index a1dd872..abfcfb3 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -4,7 +4,7 @@ const SteamID = require('steamid'); const SteamCommunity = require('../index.js'); const Helpers = require('../components/helpers.js'); -const EDiscussionType = require("../resources/EDiscussionType.js"); +const EDiscussionType = require('../resources/EDiscussionType.js'); /** @@ -39,22 +39,22 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { let $ = Cheerio.load(body); // Get breadcrumbs once. Depending on the type of discussion, it either uses "forum" or "group" breadcrumbs - let breadcrumbs = $(".forum_breadcrumbs").children(); + let breadcrumbs = $('.forum_breadcrumbs').children(); - if (breadcrumbs.length == 0) breadcrumbs = $(".group_breadcrumbs").children(); + if (breadcrumbs.length == 0) breadcrumbs = $('.group_breadcrumbs').children(); /* --------------------- Find and map values --------------------- */ // Determine type from URL as some checks will deviate, depending on the type - if (url.includes("steamcommunity.com/discussions/forum")) discussion.type = EDiscussionType.Forum; + if (url.includes('steamcommunity.com/discussions/forum')) discussion.type = EDiscussionType.Forum; if (/steamcommunity.com\/app\/.+\/discussions/g.test(url)) discussion.type = EDiscussionType.App; if (/steamcommunity.com\/groups\/.+\/discussions/g.test(url)) discussion.type = EDiscussionType.Group; // Get appID from breadcrumbs if this discussion is associated to one if (discussion.type == EDiscussionType.App) { - let appIdHref = breadcrumbs[0].attribs["href"].split("/"); + let appIdHref = breadcrumbs[0].attribs.href.split('/'); discussion.appID = appIdHref[appIdHref.length - 1]; } @@ -64,16 +64,16 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { let forumIdHref; if (discussion.type == EDiscussionType.Group) { // Groups have an extra breadcrumb so we need to shift by 2 - forumIdHref = breadcrumbs[4].attribs["href"].split("/"); + forumIdHref = breadcrumbs[4].attribs.href.split('/'); } else { - forumIdHref = breadcrumbs[2].attribs["href"].split("/"); + forumIdHref = breadcrumbs[2].attribs.href.split('/'); } discussion.forumID = forumIdHref[forumIdHref.length - 2]; // Get id, gidforum and topicOwner. The first is used in the URL itself, the other two only in post requests - let gids = $(".forum_paging > .forum_paging_controls").attr("id").split("_"); + let gids = $('.forum_paging > .forum_paging_controls').attr('id').split('_'); discussion.id = gids[4]; discussion.gidforum = gids[3]; @@ -81,33 +81,33 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { // Find postedDate and convert to timestamp - let posted = $(".topicstats > .topicstats_label:contains(\"Date Posted:\")").next().text(); + let posted = $('.topicstats > .topicstats_label:contains("Date Posted:")').next().text(); discussion.postedDate = Helpers.decodeSteamTime(posted.trim()); // Find commentsAmount - discussion.commentsAmount = Number($(".topicstats > .topicstats_label:contains(\"Posts:\")").next().text()); + discussion.commentsAmount = Number($('.topicstats > .topicstats_label:contains("Posts:")').next().text()); // Get discussion title & content - discussion.title = $(".forum_op > .topic").text().trim(); - discussion.content = $(".forum_op > .content").text().trim(); + discussion.title = $('.forum_op > .topic').text().trim(); + discussion.content = $('.forum_op > .content').text().trim(); // Find comment marked as answer - let hasAnswer = $(".commentthread_answer_bar") + let hasAnswer = $('.commentthread_answer_bar'); if (hasAnswer.length != 0) { - let answerPermLink = hasAnswer.next().children(".forum_comment_permlink").text().trim(); + let answerPermLink = hasAnswer.next().children('.forum_comment_permlink').text().trim(); // Convert comment id to number, remove hashtag and subtract by 1 to make it an index - discussion.answerCommentIndex = Number(answerPermLink.replace("#", "")) - 1; + discussion.answerCommentIndex = Number(answerPermLink.replace('#', '')) - 1; } // Find author and convert to SteamID object - let authorLink = $(".authorline > .forum_op_author").attr("href"); + let authorLink = $('.authorline > .forum_op_author').attr('href'); Helpers.resolveVanityURL(authorLink, (err, data) => { // This request takes <1 sec if (err) { @@ -124,8 +124,8 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { } catch (err) { callback(err, null); } - }, "steamcommunity"); -} + }, 'steamcommunity'); +}; /** diff --git a/components/discussions.js b/components/discussions.js index a892c06..43eeadb 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -12,10 +12,10 @@ const Helpers = require('./helpers.js'); * @param {function} callback - First argument is null/Error, second is array containing the requested comments */ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIndex, callback) { - this.httpRequestGet(url + "?l=en", async (err, res, body) => { + this.httpRequestGet(url + '?l=en', async (err, res, body) => { if (err) { - callback("Failed to load discussion: " + err, null); + callback('Failed to load discussion: ' + err, null); return; } @@ -23,20 +23,20 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn // Load output into cheerio to make parsing easier let $ = Cheerio.load(body); - let paging = $(".forum_paging > .forum_paging_summary").children(); + let paging = $('.forum_paging > .forum_paging_summary').children(); /** * Stores every loaded page inside a Cheerio instance * @type {{[key: number]: cheerio.Root}} */ - let pages = { + let pages = { 0: $ }; // Determine amount of comments per page and total. Update endIndex if null to get all comments let commentsPerPage = Number(paging[4].children[0].data); - let totalComments = Number(paging[5].children[0].data) + let totalComments = Number(paging[5].children[0].data); if (endIndex == null || endIndex > totalComments - 1) { // Make sure to check against null as the index 0 would cast to false endIndex = totalComments - 1; @@ -54,14 +54,14 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn promises.push(new Promise((resolve) => { setTimeout(() => { // Delay fetching a bit to reduce the risk of Steam blocking us - this.httpRequestGet(url + "?l=en&ctp=" + (i + 1), (err, res, body) => { + this.httpRequestGet(url + '?l=en&ctp=' + (i + 1), (err, res, body) => { try { pages[i] = Cheerio.load(body); resolve(); } catch (err) { - return callback("Failed to load comments page: " + err, null); + return callback('Failed to load comments page: ' + err, null); } - }, "steamcommunity"); + }, 'steamcommunity'); }, 250 * i); })); @@ -77,29 +77,29 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn let $ = pages[Math.trunc(i / commentsPerPage)]; let thisComment = $(`.forum_comment_permlink:contains("#${i + 1}")`).parent(); - let thisCommentID = thisComment.attr("id").replace("comment_", ""); + let thisCommentID = thisComment.attr('id').replace('comment_', ''); // Note: '>' inside the cheerio selectors didn't work here - let authorContainer = thisComment.children(".commentthread_comment_content").children(".commentthread_comment_author").children(".commentthread_author_link"); - let commentContainer = thisComment.children(".commentthread_comment_content").children(`#comment_content_${thisCommentID}`); + let authorContainer = thisComment.children('.commentthread_comment_content').children('.commentthread_comment_author').children('.commentthread_author_link'); + let commentContainer = thisComment.children('.commentthread_comment_content').children(`#comment_content_${thisCommentID}`); // Prepare comment text by finding all existing blockquotes, formatting them and adding them infront each other. Afterwards handle the text itself - let commentText = ""; - let blockQuoteSelector = ".bb_blockquote"; + let commentText = ''; + let blockQuoteSelector = '.bb_blockquote'; let children = commentContainer.children(blockQuoteSelector); for (let i = 0; i < 10; i++) { // I'm not sure how I could dynamically check the amount of nested blockquotes. 10 is prob already too much to stay readable if (children.length > 0) { - let thisQuoteText = ""; + let thisQuoteText = ''; - thisQuoteText += children.children(".bb_quoteauthor").text() + "\n"; // Get quote header and add a proper newline + thisQuoteText += children.children('.bb_quoteauthor').text() + '\n'; // Get quote header and add a proper newline // Replace
's with newlines to get a proper output - let quoteWithNewlines = children.first().find("br").replaceWith("\n"); + let quoteWithNewlines = children.first().find('br').replaceWith('\n'); - thisQuoteText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text' }).text().trim(); // Get blockquote content without child content - https://stackoverflow.com/a/23956052 - if (i > 0) thisQuoteText += "\n-------\n"; // Add spacer + thisQuoteText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text'; }).text().trim(); // Get blockquote content without child content - https://stackoverflow.com/a/23956052 + if (i > 0) thisQuoteText += '\n-------\n'; // Add spacer commentText = thisQuoteText + commentText; // Concat quoteText to the start of commentText as the most nested quote is the first one inside the comment chain itself @@ -108,31 +108,31 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn } else { - commentText += "\n\n-------\n\n"; // Add spacer + commentText += '\n\n-------\n\n'; // Add spacer break; } } - let quoteWithNewlines = commentContainer.first().find("br").replaceWith("\n"); // Replace
's with newlines to get a proper output + let quoteWithNewlines = commentContainer.first().find('br').replaceWith('\n'); // Replace
's with newlines to get a proper output - commentText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text' }).text().trim(); // Add comment content without child content - https://stackoverflow.com/a/23956052 + commentText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text'; }).text().trim(); // Add comment content without child content - https://stackoverflow.com/a/23956052 comments.push({ index: i, commentId: thisCommentID, commentLink: `${url}#c${thisCommentID}`, - authorLink: authorContainer.attr("href"), // I did not call 'resolveVanityURL()' here and convert to SteamID to reduce the amount of potentially unused Steam pings - postedDate: Helpers.decodeSteamTime(authorContainer.children(".commentthread_comment_timestamp").text().trim()), + authorLink: authorContainer.attr('href'), // I did not call 'resolveVanityURL()' here and convert to SteamID to reduce the amount of potentially unused Steam pings + postedDate: Helpers.decodeSteamTime(authorContainer.children('.commentthread_comment_timestamp').text().trim()), content: commentText.trim() }); } - + // Callback our result callback(null, comments); - }, "steamcommunity"); + }, 'steamcommunity'); }; /** @@ -145,16 +145,16 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn */ SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, discussionId, message, callback) { this.httpRequestPost({ - "uri": `https://steamcommunity.com/comment/ForumTopic/post/${topicOwner}/${gidforum}/`, - "form": { - "comment": message, - "count": 15, - "sessionid": this.getSessionID(), - "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - "feature2": discussionId, - "json": 1 + uri: `https://steamcommunity.com/comment/ForumTopic/post/${topicOwner}/${gidforum}/`, + form: { + comment: message, + count: 15, + sessionid: this.getSessionID(), + extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + feature2: discussionId, + json: 1 }, - "json": true + json: true }, function(err, response, body) { if (!callback) { return; @@ -170,7 +170,7 @@ SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, } else { callback(new Error(body.error)); } - }, "steamcommunity"); + }, 'steamcommunity'); }; /** @@ -183,16 +183,16 @@ SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, */ SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum, discussionId, gidcomment, callback) { this.httpRequestPost({ - "uri": `https://steamcommunity.com/comment/ForumTopic/delete/${topicOwner}/${gidforum}/`, - "form": { - "gidcomment": gidcomment, - "count": 15, - "sessionid": this.getSessionID(), - "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - "feature2": discussionId, - "json": 1 + uri: `https://steamcommunity.com/comment/ForumTopic/delete/${topicOwner}/${gidforum}/`, + form: { + gidcomment: gidcomment, + count: 15, + sessionid: this.getSessionID(), + extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + feature2: discussionId, + json: 1 }, - "json": true + json: true }, function(err, response, body) { // Steam does not seem to return any errors here even when trying to delete a non-existing comment but let's check the response anyway if (!callback) { return; @@ -208,7 +208,7 @@ SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum } else { callback(new Error(body.error)); } - }, "steamcommunity"); + }, 'steamcommunity'); }; /** @@ -220,15 +220,15 @@ SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum */ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidforum, discussionId, callback) { this.httpRequestPost({ - "uri": `https://steamcommunity.com/comment/ForumTopic/subscribe/${topicOwner}/${gidforum}/`, - "form": { - "count": 15, - "sessionid": this.getSessionID(), - "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - "feature2": discussionId, - "json": 1 + uri: `https://steamcommunity.com/comment/ForumTopic/subscribe/${topicOwner}/${gidforum}/`, + form: { + count: 15, + sessionid: this.getSessionID(), + extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + feature2: discussionId, + json: 1 }, - "json": true + json: true }, function(err, response, body) { if (!callback) { return; @@ -245,7 +245,7 @@ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidf } callback(null); - }, "steamcommunity"); + }, 'steamcommunity'); }; /** @@ -257,15 +257,15 @@ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidf */ SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gidforum, discussionId, callback) { this.httpRequestPost({ - "uri": `https://steamcommunity.com/comment/ForumTopic/unsubscribe/${topicOwner}/${gidforum}/`, - "form": { - "count": 15, - "sessionid": this.getSessionID(), - "extended_data": '{}', // Unsubscribing does not require any data here - "feature2": discussionId, - "json": 1 + uri: `https://steamcommunity.com/comment/ForumTopic/unsubscribe/${topicOwner}/${gidforum}/`, + form: { + count: 15, + sessionid: this.getSessionID(), + extended_data: '{}', // Unsubscribing does not require any data here + feature2: discussionId, + json: 1 }, - "json": true + json: true }, function(err, response, body) { if (!callback) { return; @@ -282,7 +282,7 @@ SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gi } callback(null); - }, "steamcommunity"); + }, 'steamcommunity'); }; /** @@ -291,16 +291,16 @@ SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gi * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.setDiscussionCommentsPerPage = function(value, callback) { - if (!["15", "30", "50"].includes(value)) value = "50"; // Check for invalid setting + if (!['15', '30', '50'].includes(value)) value = '50'; // Check for invalid setting this.httpRequestPost({ - "uri": `https://steamcommunity.com/forum/0/0/setpreference`, - "form": { - "preference": "topicrepliesperpage", - "value": value, - "sessionid": this.getSessionID(), + uri: 'https://steamcommunity.com/forum/0/0/setpreference', + form: { + preference: 'topicrepliesperpage', + value: value, + sessionid: this.getSessionID(), }, - "json": true + json: true }, function(err, response, body) { // Steam does not seem to return any errors for this request if (!callback) { return; @@ -317,5 +317,5 @@ SteamCommunity.prototype.setDiscussionCommentsPerPage = function(value, callback } callback(null); - }, "steamcommunity"); + }, 'steamcommunity'); }; \ No newline at end of file diff --git a/resources/EDiscussionType.js b/resources/EDiscussionType.js index b6c28bb..99bcd60 100644 --- a/resources/EDiscussionType.js +++ b/resources/EDiscussionType.js @@ -2,12 +2,12 @@ * @enum EDiscussionType */ module.exports = { - "Forum": 0, - "App": 1, - "Group": 2, + Forum: 0, + App: 1, + Group: 2, // Value-to-name mapping for convenience - "0": "Forum", - "1": "App", - "2": "Group" + 0: 'Forum', + 1: 'App', + 2: 'Group' }; \ No newline at end of file From 64eaf1b09272d1072b73518f327384dfe7ce22ab Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:09:10 +0200 Subject: [PATCH 17/21] Return 'Discussion Not Found' error when breadcrumbs are missing to avoid unhandled exceptions --- classes/CSteamDiscussion.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index abfcfb3..8168b55 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -31,6 +31,11 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { // Get DOM of discussion this.httpRequestGet(url, (err, res, body) => { + if (err) { + callback(err); + return; + } + try { /* --------------------- Preprocess output --------------------- */ @@ -43,6 +48,11 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { if (breadcrumbs.length == 0) breadcrumbs = $('.group_breadcrumbs').children(); + // Steam redirects us to the forum page if the discussion does not exist which we can detect by missing breadcrumbs + if (!breadcrumbs[0]) { + callback(new Error('Discussion not found')); + return; + } /* --------------------- Find and map values --------------------- */ From e3dbb2c4935cf702231aebbda3e7ce8a2ac24b11 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Mon, 2 Oct 2023 20:12:40 +0200 Subject: [PATCH 18/21] Rename discussion postComment() -> comment() --- classes/CSteamDiscussion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index 8168b55..cefee03 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -171,7 +171,7 @@ CSteamDiscussion.prototype.getComments = function(startIndex, endIndex, callback * @param {String} message - Content of the comment to post * @param {function} callback - Takes only an Error object/null as the first argument */ -CSteamDiscussion.prototype.postComment = function(message, callback) { +CSteamDiscussion.prototype.comment = function(message, callback) { this._community.postDiscussionComment(this.topicOwner, this.gidforum, this.id, message, callback); }; From 983ae0fbb6234842b014645102a77301c64e068e Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Mon, 2 Oct 2023 20:21:15 +0200 Subject: [PATCH 19/21] Update httpRequest helper usage to v4 --- classes/CSteamDiscussion.js | 27 ++-- components/discussions.js | 253 ++++++++++++++++-------------------- 2 files changed, 129 insertions(+), 151 deletions(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index cefee03..18082ac 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -1,5 +1,6 @@ const Cheerio = require('cheerio'); const SteamID = require('steamid'); +const StdLib = require('@doctormckay/stdlib'); const SteamCommunity = require('../index.js'); const Helpers = require('../components/helpers.js'); @@ -30,18 +31,20 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { }; // Get DOM of discussion - this.httpRequestGet(url, (err, res, body) => { - if (err) { - callback(err); - return; - } + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let result = await this.httpRequest({ + method: 'GET', + url: url, + source: 'steamcommunity' + }); + try { /* --------------------- Preprocess output --------------------- */ // Load output into cheerio to make parsing easier - let $ = Cheerio.load(body); + let $ = Cheerio.load(result.textBody); // Get breadcrumbs once. Depending on the type of discussion, it either uses "forum" or "group" breadcrumbs let breadcrumbs = $('.forum_breadcrumbs').children(); @@ -50,7 +53,7 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { // Steam redirects us to the forum page if the discussion does not exist which we can detect by missing breadcrumbs if (!breadcrumbs[0]) { - callback(new Error('Discussion not found')); + reject(new Error('Discussion not found')); return; } @@ -121,20 +124,20 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { Helpers.resolveVanityURL(authorLink, (err, data) => { // This request takes <1 sec if (err) { - callback(err); + reject(err); return; } discussion.author = new SteamID(data.steamID); - // Make callback when ID was resolved as otherwise owner will always be null - callback(null, new CSteamDiscussion(this, discussion)); + // Resolve when ID was resolved as otherwise owner will always be null + resolve(new CSteamDiscussion(this, discussion)); }); } catch (err) { - callback(err, null); + reject(err); } - }, 'steamcommunity'); + }); }; diff --git a/components/discussions.js b/components/discussions.js index 43eeadb..0263805 100644 --- a/components/discussions.js +++ b/components/discussions.js @@ -1,4 +1,5 @@ const Cheerio = require('cheerio'); +const StdLib = require('@doctormckay/stdlib'); const SteamCommunity = require('../index.js'); const Helpers = require('./helpers.js'); @@ -12,16 +13,16 @@ const Helpers = require('./helpers.js'); * @param {function} callback - First argument is null/Error, second is array containing the requested comments */ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIndex, callback) { - this.httpRequestGet(url + '?l=en', async (err, res, body) => { - - if (err) { - callback('Failed to load discussion: ' + err, null); - return; - } + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let result = await this.httpRequest({ + method: 'GET', + url: url + '?l=en', + source: 'steamcommunity' + }); // Load output into cheerio to make parsing easier - let $ = Cheerio.load(body); + let $ = Cheerio.load(result.textBody); let paging = $('.forum_paging > .forum_paging_summary').children(); @@ -52,16 +53,20 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn if (i == 0) continue; // First page is already in pages object promises.push(new Promise((resolve) => { - setTimeout(() => { // Delay fetching a bit to reduce the risk of Steam blocking us + setTimeout(async () => { // Delay fetching a bit to reduce the risk of Steam blocking us - this.httpRequestGet(url + '?l=en&ctp=' + (i + 1), (err, res, body) => { - try { - pages[i] = Cheerio.load(body); - resolve(); - } catch (err) { - return callback('Failed to load comments page: ' + err, null); - } - }, 'steamcommunity'); + let commentsPage = await this.httpRequest({ + method: 'GET', + url: url + '?l=en&ctp=' + (i + 1), + source: 'steamcommunity' + }); + + try { + pages[i] = Cheerio.load(commentsPage.textBody); + resolve(); + } catch (err) { + return reject('Failed to load comments page: ' + err); + } }, 250 * i); })); @@ -129,10 +134,10 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn } - // Callback our result - callback(null, comments); + // Resolve with our result + resolve(comments); - }, 'steamcommunity'); + }); }; /** @@ -144,33 +149,27 @@ SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIn * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, discussionId, message, callback) { - this.httpRequestPost({ - uri: `https://steamcommunity.com/comment/ForumTopic/post/${topicOwner}/${gidforum}/`, - form: { - comment: message, - count: 15, - sessionid: this.getSessionID(), - extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - feature2: discussionId, - json: 1 - }, - json: true - }, function(err, response, body) { - if (!callback) { - return; - } - - if (err) { - callback(err); - return; - } - - if (body.success) { - callback(null); + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let result = await this.httpRequest({ + method: 'POST', + url: `https://steamcommunity.com/comment/ForumTopic/post/${topicOwner}/${gidforum}/`, + form: { + comment: message, + count: 15, + sessionid: this.getSessionID(), + extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + feature2: discussionId, + json: 1 + }, + source: 'steamcommunity' + }); + + if (result.jsonBody.success) { + resolve(null); } else { - callback(new Error(body.error)); + reject(new Error(result.jsonBody.error)); } - }, 'steamcommunity'); + }); }; /** @@ -182,33 +181,27 @@ SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum, discussionId, gidcomment, callback) { - this.httpRequestPost({ - uri: `https://steamcommunity.com/comment/ForumTopic/delete/${topicOwner}/${gidforum}/`, - form: { - gidcomment: gidcomment, - count: 15, - sessionid: this.getSessionID(), - extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - feature2: discussionId, - json: 1 - }, - json: true - }, function(err, response, body) { // Steam does not seem to return any errors here even when trying to delete a non-existing comment but let's check the response anyway - if (!callback) { - return; - } - - if (err) { - callback(err); - return; - } - - if (body.success) { - callback(null); + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let result = await this.httpRequest({ + method: 'POST', + url: `https://steamcommunity.com/comment/ForumTopic/delete/${topicOwner}/${gidforum}/`, + form: { + gidcomment: gidcomment, + count: 15, + sessionid: this.getSessionID(), + extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + feature2: discussionId, + json: 1 + }, + source: 'steamcommunity' + }); + + if (result.jsonBody.success) { + resolve(null); } else { - callback(new Error(body.error)); + reject(new Error(result.jsonBody.error)); } - }, 'steamcommunity'); + }); }; /** @@ -219,33 +212,27 @@ SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidforum, discussionId, callback) { - this.httpRequestPost({ - uri: `https://steamcommunity.com/comment/ForumTopic/subscribe/${topicOwner}/${gidforum}/`, - form: { - count: 15, - sessionid: this.getSessionID(), - extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', - feature2: discussionId, - json: 1 - }, - json: true - }, function(err, response, body) { - if (!callback) { - return; - } - - if (err) { - callback(err); - return; - } - - if (body.success && body.success != SteamCommunity.EResult.OK) { - callback(Helpers.eresultError(body.success)); + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let result = await this.httpRequest({ + method: 'POST', + url: `https://steamcommunity.com/comment/ForumTopic/subscribe/${topicOwner}/${gidforum}/`, + form: { + count: 15, + sessionid: this.getSessionID(), + extended_data: '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + feature2: discussionId, + json: 1 + }, + source: 'steamcommunity' + }); + + if (result.jsonBody.success && result.jsonBody.success != SteamCommunity.EResult.OK) { + reject(Helpers.eresultError(result.jsonBody.success)); return; } - callback(null); - }, 'steamcommunity'); + resolve(null); + }); }; /** @@ -256,33 +243,27 @@ SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidf * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gidforum, discussionId, callback) { - this.httpRequestPost({ - uri: `https://steamcommunity.com/comment/ForumTopic/unsubscribe/${topicOwner}/${gidforum}/`, - form: { - count: 15, - sessionid: this.getSessionID(), - extended_data: '{}', // Unsubscribing does not require any data here - feature2: discussionId, - json: 1 - }, - json: true - }, function(err, response, body) { - if (!callback) { + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let result = await this.httpRequest({ + method: 'POST', + url: `https://steamcommunity.com/comment/ForumTopic/unsubscribe/${topicOwner}/${gidforum}/`, + form: { + count: 15, + sessionid: this.getSessionID(), + extended_data: '{}', // Unsubscribing does not require any data here + feature2: discussionId, + json: 1 + }, + source: 'steamcommunity' + }); + + if (result.jsonBody.success && result.jsonBody.success != SteamCommunity.EResult.OK) { + reject(Helpers.eresultError(result.jsonBody.success)); return; } - if (err) { - callback(err); - return; - } - - if (body.success && body.success != SteamCommunity.EResult.OK) { - callback(Helpers.eresultError(body.success)); - return; - } - - callback(null); - }, 'steamcommunity'); + resolve(null); + }); }; /** @@ -293,29 +274,23 @@ SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gi SteamCommunity.prototype.setDiscussionCommentsPerPage = function(value, callback) { if (!['15', '30', '50'].includes(value)) value = '50'; // Check for invalid setting - this.httpRequestPost({ - uri: 'https://steamcommunity.com/forum/0/0/setpreference', - form: { - preference: 'topicrepliesperpage', - value: value, - sessionid: this.getSessionID(), - }, - json: true - }, function(err, response, body) { // Steam does not seem to return any errors for this request - if (!callback) { - return; - } - - if (err) { - callback(err); - return; - } - - if (body.success && body.success != SteamCommunity.EResult.OK) { - callback(Helpers.eresultError(body.success)); + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let result = await this.httpRequest({ + method: 'POST', + url: 'https://steamcommunity.com/forum/0/0/setpreference', + form: { + preference: 'topicrepliesperpage', + value: value, + sessionid: this.getSessionID(), + }, + source: 'steamcommunity' + }); + + if (result.jsonBody.success && result.jsonBody.success != SteamCommunity.EResult.OK) { + reject(Helpers.eresultError(result.jsonBody.success)); return; } - callback(null); - }, 'steamcommunity'); + resolve(null); + }); }; \ No newline at end of file From 3c7972ae173736275ac2630f590d6112fdecabb1 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sat, 2 Mar 2024 12:33:39 +0100 Subject: [PATCH 20/21] feat(Discussions): Add eventcomments support --- classes/CSteamDiscussion.js | 45 +++++++++++++++++++++--------------- resources/EDiscussionType.js | 6 +++-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index 18082ac..bf0775c 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -63,6 +63,7 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { if (url.includes('steamcommunity.com/discussions/forum')) discussion.type = EDiscussionType.Forum; if (/steamcommunity.com\/app\/.+\/discussions/g.test(url)) discussion.type = EDiscussionType.App; if (/steamcommunity.com\/groups\/.+\/discussions/g.test(url)) discussion.type = EDiscussionType.Group; + if (/steamcommunity.com\/app\/.+\/eventcomments/g.test(url)) discussion.type = EDiscussionType.Eventcomments; // Get appID from breadcrumbs if this discussion is associated to one @@ -73,16 +74,18 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { } - // Get forumID from breadcrumbs - let forumIdHref; + // Get forumID from breadcrumbs - Ignore for type Eventcomments as it doesn't have multiple forums + if (discussion.type != EDiscussionType.Eventcomments) { + let forumIdHref; - if (discussion.type == EDiscussionType.Group) { // Groups have an extra breadcrumb so we need to shift by 2 - forumIdHref = breadcrumbs[4].attribs.href.split('/'); - } else { - forumIdHref = breadcrumbs[2].attribs.href.split('/'); - } + if (discussion.type == EDiscussionType.Group) { // Groups have an extra breadcrumb so we need to shift by 2 + forumIdHref = breadcrumbs[4].attribs.href.split('/'); + } else { + forumIdHref = breadcrumbs[2].attribs.href.split('/'); + } - discussion.forumID = forumIdHref[forumIdHref.length - 2]; + discussion.forumID = forumIdHref[forumIdHref.length - 2]; + } // Get id, gidforum and topicOwner. The first is used in the URL itself, the other two only in post requests @@ -119,20 +122,24 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { } - // Find author and convert to SteamID object - let authorLink = $('.authorline > .forum_op_author').attr('href'); + // Find author and convert to SteamID object - Ignore for type Eventcomments as they are posted by the "game", not by an Individual + if (discussion.type != EDiscussionType.Eventcomments) { + let authorLink = $('.authorline > .forum_op_author').attr('href'); - Helpers.resolveVanityURL(authorLink, (err, data) => { // This request takes <1 sec - if (err) { - reject(err); - return; - } + Helpers.resolveVanityURL(authorLink, (err, data) => { // This request takes <1 sec + if (err) { + reject(err); + return; + } - discussion.author = new SteamID(data.steamID); + discussion.author = new SteamID(data.steamID); - // Resolve when ID was resolved as otherwise owner will always be null + // Resolve when ID was resolved as otherwise owner will always be null + resolve(new CSteamDiscussion(this, discussion)); + }); + } else { resolve(new CSteamDiscussion(this, discussion)); - }); + } } catch (err) { reject(err); @@ -145,7 +152,7 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { * Constructor - Creates a new Discussion object * @class * @param {SteamCommunity} community - * @param {{ id: string, appID: string, forumID: string, author: SteamID, postedDate: Object, title: string, content: string, commentsAmount: number }} data + * @param {{ id: string, type: EDiscussionType, appID: string, forumID: string, gidforum: string, topicOwner: string, author: SteamID, postedDate: Object, title: string, content: string, commentsAmount: number, answerCommentIndex: number }} data */ function CSteamDiscussion(community, data) { /** diff --git a/resources/EDiscussionType.js b/resources/EDiscussionType.js index 99bcd60..30996bd 100644 --- a/resources/EDiscussionType.js +++ b/resources/EDiscussionType.js @@ -5,9 +5,11 @@ module.exports = { Forum: 0, App: 1, Group: 2, + Eventcomments: 3, // Value-to-name mapping for convenience 0: 'Forum', 1: 'App', - 2: 'Group' -}; \ No newline at end of file + 2: 'Group', + 3: 'Eventcomments' +}; From 985c2a54d743b0ae3c9c33e63dd3c2a1a60b1466 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+3urobeat@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:18:30 +0100 Subject: [PATCH 21/21] feat(Discussions): Add accountCanComment prop --- classes/CSteamDiscussion.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/classes/CSteamDiscussion.js b/classes/CSteamDiscussion.js index bf0775c..b6706b2 100644 --- a/classes/CSteamDiscussion.js +++ b/classes/CSteamDiscussion.js @@ -27,14 +27,15 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { title: null, content: null, commentsAmount: null, // I originally wanted to fetch all comments by default but that would have been a lot of potentially unused data - answerCommentIndex: null + answerCommentIndex: null, + accountCanComment: null // Is this account allowed to comment on this discussion? }; // Get DOM of discussion return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { let result = await this.httpRequest({ method: 'GET', - url: url, + url: url + '?l=en', source: 'steamcommunity' }); @@ -122,6 +123,12 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { } + // Check if this account is allowed to comment on this discussion + let cannotReplyReason = $('.topic_cannotreply_reason'); + + discussion.accountCanComment = cannotReplyReason.length == 0; + + // Find author and convert to SteamID object - Ignore for type Eventcomments as they are posted by the "game", not by an Individual if (discussion.type != EDiscussionType.Eventcomments) { let authorLink = $('.authorline > .forum_op_author').attr('href'); @@ -152,7 +159,7 @@ SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { * Constructor - Creates a new Discussion object * @class * @param {SteamCommunity} community - * @param {{ id: string, type: EDiscussionType, appID: string, forumID: string, gidforum: string, topicOwner: string, author: SteamID, postedDate: Object, title: string, content: string, commentsAmount: number, answerCommentIndex: number }} data + * @param {{ id: string, type: EDiscussionType, appID: string, forumID: string, gidforum: string, topicOwner: string, author: SteamID, postedDate: Object, title: string, content: string, commentsAmount: number, answerCommentIndex: number, accountCanComment: boolean }} data */ function CSteamDiscussion(community, data) { /**