From 7cc7bcfdd25075941159006f454a7bc7c3696c79 Mon Sep 17 00:00:00 2001 From: Erki Date: Thu, 21 May 2020 13:45:07 +0300 Subject: [PATCH] Implements the pulling of news from the forums (#8887) --- SQL/migrate/V050__Remove_news_table.sql | 6 + aurorastation.dme | 3 +- code/controllers/configuration.dm | 14 ++ code/controllers/subsystems/news.dm | 154 ++++++++++++------ code/datums/discord/bot.dm | 8 +- code/datums/discord/webhook.dm | 2 +- code/game/machinery/newscaster.dm | 2 +- code/modules/http/forum_api.dm | 51 ++++++ .../subsystems => modules/http}/http.dm | 35 ++-- config/example/config.txt | 15 ++ config/example/news.json | 8 + html/changelogs/skull132_forum-news.yml | 5 + 12 files changed, 227 insertions(+), 76 deletions(-) create mode 100644 SQL/migrate/V050__Remove_news_table.sql create mode 100644 code/modules/http/forum_api.dm rename code/{controllers/subsystems => modules/http}/http.dm (58%) create mode 100644 config/example/news.json create mode 100644 html/changelogs/skull132_forum-news.yml diff --git a/SQL/migrate/V050__Remove_news_table.sql b/SQL/migrate/V050__Remove_news_table.sql new file mode 100644 index 00000000000..763a86a0a3e --- /dev/null +++ b/SQL/migrate/V050__Remove_news_table.sql @@ -0,0 +1,6 @@ +-- +-- Drops the SQL table for news in lieu of using the forums. +-- + +DROP TABLE `ss13_news_stories`; +DROP TABLE `ss13_news_channels`; diff --git a/aurorastation.dme b/aurorastation.dme index af98681d0e4..b608b3e2674 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -186,7 +186,6 @@ #include "code\controllers\subsystems\ghostroles.dm" #include "code\controllers\subsystems\global_listener.dm" #include "code\controllers\subsystems\hallucinations.dm" -#include "code\controllers\subsystems\http.dm" #include "code\controllers\subsystems\icon_cache.dm" #include "code\controllers\subsystems\icon_smooth.dm" #include "code\controllers\subsystems\icon_updates.dm" @@ -1637,6 +1636,8 @@ #include "code\modules\holodeck\HolodeckControl.dm" #include "code\modules\holodeck\HolodeckObjects.dm" #include "code\modules\holodeck\HolodeckPrograms.dm" +#include "code\modules\http\forum_api.dm" +#include "code\modules\http\http.dm" #include "code\modules\hydroponics\grown.dm" #include "code\modules\hydroponics\grown_inedible.dm" #include "code\modules\hydroponics\grown_predefined.dm" diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index d7f01e00ef8..2623d95965a 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -304,6 +304,11 @@ var/list/gamemode_cache = list() var/time_to_call_emergency_shuttle = 36000 //how many time until the crew can call the transfer shuttle. One hour by default. + var/forum_api_path + // global.forum_api_key - see modules/http/forum_api.dm + + var/news_use_forum_api = FALSE + /datum/configuration/New() var/list/L = typesof(/datum/game_mode) - /datum/game_mode for (var/T in L) @@ -919,6 +924,15 @@ var/list/gamemode_cache = list() if ("fail2topic_enabled") fail2topic_enabled = text2num(value) + + if ("forum_api_path") + forum_api_path = value + if ("forum_api_key") + global.forum_api_key = value + + if ("news_use_forum_api") + news_use_forum_api = TRUE + else log_misc("Unknown setting in configuration: '[name]'") diff --git a/code/controllers/subsystems/news.dm b/code/controllers/subsystems/news.dm index d4553a83638..6ac1d982948 100644 --- a/code/controllers/subsystems/news.dm +++ b/code/controllers/subsystems/news.dm @@ -5,6 +5,7 @@ flags = SS_NO_FIRE var/list/datum/feed_channel/network_channels = list() var/datum/feed_message/wanted_issue + var/list/forum_topics /datum/controller/subsystem/news/Recover() src.network_channels = SSnews.network_channels @@ -17,60 +18,97 @@ CreateFeedChannel("Station Announcements", "Automatic Announcement System", 1, 1, "New Station Announcement Available") CreateFeedChannel("Tau Ceti Daily", "CentComm Minister of Information", 1, 1) CreateFeedChannel("The Gibson Gazette", "Editor Carl Ritz", 1, 1) - if(config.sql_enabled) - load_from_sql() + + if (config.news_use_forum_api) + load_forum_news_config() + + load_from_forums() + ..() -/datum/controller/subsystem/news/proc/load_from_sql() - if(!establish_db_connection(dbcon)) - log_debug("SSnews: SQL ERROR - Failed to connect.") +/datum/controller/subsystem/news/proc/load_from_forums() + if (!config.forum_api_path || !global.forum_api_key) + log_debug("SSnews: Unable to load from forums, API path or key not set up.") return - var/DBQuery/channel_query = dbcon.NewQuery("SELECT id, name, author, locked, is_admin_channel, announcement FROM ss13_news_channels WHERE deleted_at IS NULL ORDER BY name ASC") - channel_query.Execute() - while(channel_query.NextRow()) - CHECK_TICK - var/datum/feed_channel/channel = null - try - channel = CreateFeedChannel( - channel_query.item[2], - channel_query.item[3], - text2num(channel_query.item[4]), - text2num(channel_query.item[5]), - channel_query.item[6]) - catch(var/exception/ec) - log_debug("SSnews: Error when loading channel: [ec]") + + if (!length(forum_topics)) + return + + for (var/topic_id in forum_topics) + var/datum/http_request/forum_api/initial = new("forums/topics") + initial.prepare_get("[topic_id]") + initial.begin_async() + UNTIL(initial.is_complete()) + + var/datum/http_response/initial_response = initial.into_response() + if (initial_response.errored) + testing("Errored: [initial_response.error]") + log_debug("SSnews: errored: [initial_response.error]") continue - //Load the expired news into the modular computers first - var/news_count = 1 - var/DBQuery/news_query_expired = dbcon.NewQuery("SELECT body FROM ss13_news_stories WHERE deleted_at IS NULL AND channel_id = :channel_id: AND publish_at < NOW() AND (publish_until <= NOW()) AND approved_at IS NOT NULL ORDER BY publish_at DESC") - news_query_expired.Execute(list("channel_id" = channel_query.item[1])) - while(news_query_expired.NextRow()) - CHECK_TICK - try - var/datum/computer_file/data/news_article/news = new() - news.filename = "[channel.channel_name] vol. [news_count]" - news.archived = 1 - news.stored_data = news_query_expired.item[1] - ntnet_global.available_news.Add(news) - news_count += 1 - catch(var/exception/em) - log_debug("SSnews: Error when loading news: [em]") - var/DBQuery/news_query = dbcon.NewQuery("SELECT body, author, is_admin_message, message_type, ic_timestamp, url FROM ss13_news_stories WHERE deleted_at IS NULL AND channel_id = :channel_id: AND publish_at < NOW() AND (publish_until > NOW() OR publish_until IS NULL) AND approved_at IS NOT NULL ORDER BY publish_at DESC") - news_query.Execute(list("channel_id" = channel_query.item[1])) - while(news_query.NextRow()) - CHECK_TICK - try - SubmitArticle(news_query.item[1], news_query.item[2], channel, null, text2num(news_query.item[3]), news_query.item[4], news_query.item[5]) - var/datum/computer_file/data/news_article/news = new() - news.filename = "[channel.channel_name] vol. [news_count]" + var/list/forum_topic = initial_response.body + var/datum/feed_channel/channel = CreateFeedChannel( + forum_topic["title"], "System", TRUE, TRUE, FALSE + ) + + var/datum/http_request/forum_api/posts = new("forums/topics") + posts.prepare_get("[topic_id]/posts", list("sortDir" = "desc", "hidden" = 0, "page" = 1, "perPage" = 20)) + posts.begin_async() + UNTIL(posts.is_complete()) + + var/datum/http_response/posts_response = posts.into_response() + if (posts_response.errored) + log_debug("SSnews: errored getting posts from [topic_id]: [posts_response.error]") + continue + + var/list/forum_posts = posts_response.body + + var/news_count = 1 + var/archive_limit = 10 + var/total_vol_count = forum_posts["totalResults"] + var/count_pulled = length(forum_posts["results"]) + + if (total_vol_count < 20) + archive_limit = total_vol_count - archive_limit + + for (var/i = count_pulled; i > 0; i--) + var/list/post = forum_posts["results"][i] + + if (news_count > archive_limit) + SubmitArticle( + post["content"], GetForumAuthor(topic_id, post["id"]), channel, + null, FALSE, "Story", GetForumTimestamp(post["date"]) + ) + + var/datum/computer_file/data/news_article/news = new() + news.filename = "[channel.channel_name] vol. [total_vol_count - count_pulled + news_count]" + news.stored_data = post["content"] + ntnet_global.available_news.Add(news) + + if (news_count > archive_limit) + news.archived = 1 + else news.archived = 0 - news.stored_data = news_query.item[1] - ntnet_global.available_news.Add(news) - news_count += 1 - catch(var/exception/en) - log_debug("SSnews: Error when loading news: [en]") + + news_count++ + +/datum/controller/subsystem/news/proc/load_forum_news_config() + var/json = file2text("config/news.json") + + if (!length(json)) + return + + try + var/list/data = json_decode(json) + if (!data["news_topics"] || !length(data["news_topics"])) + return + + forum_topics = data["news_topics"] + forum_topics -= "_comment" + + catch(var/exception/e) + log_debug("SSnews: error loading news.json. [e]") /datum/controller/subsystem/news/proc/GetFeedChannel(var/channel_name) if(network_channels[channel_name]) @@ -133,6 +171,28 @@ for(var/obj/item/device/pda/PDA in receiving_pdas) PDA.new_news(annoncement) +/datum/controller/subsystem/news/proc/GetForumAuthor(topic_id, post_id) + topic_id = "[topic_id]" + post_id = text2num(post_id) + + if (!forum_topics[topic_id]) + return prob(50) ? "John Doe" : "Jane Doe" + + var/list/authors = forum_topics[topic_id] + var/idx = post_id % authors.len + return authors[idx + 1] + +/datum/controller/subsystem/news/proc/GetForumTimestamp(timestamp) + // Input format is: 2020-05-10T16:34:50Z + + timestamp = replacetextEx(timestamp, "T", " ") + timestamp = replacetextEx(timestamp, "Z", "") + + var/year = text2num(copytext(timestamp, 1, 5)) + var/new_year = year + 442 + timestamp = replacetext(timestamp, "[year]", "[new_year]") + + return timestamp /datum/feed_message var/author ="" diff --git a/code/datums/discord/bot.dm b/code/datums/discord/bot.dm index 1eb78ad938c..0093bfa5e05 100644 --- a/code/datums/discord/bot.dm +++ b/code/datums/discord/bot.dm @@ -312,7 +312,7 @@ var/datum/discord_bot/discord_bot = null if (!token || !message) return ERROR_PROC - var/datum/http_request/req = SShttp.post("https://discordapp.com/api/channels/[id]/messages", message, list("Authorization" = "Bot [token]", "Content-Type" = "application/json")) + var/datum/http_request/req = http_create_post("https://discordapp.com/api/channels/[id]/messages", message, list("Authorization" = "Bot [token]", "Content-Type" = "application/json")) req.begin_async() UNTIL(req.is_complete()) @@ -343,7 +343,7 @@ var/datum/discord_bot/discord_bot = null * Num upon failure. */ /datum/discord_channel/proc/get_pins(var/token) - var/datum/http_request/req = SShttp.get("https://discordapp.com/api/channels/[id]/pins", headers = list("Authorization" = "Bot [token]")) + var/datum/http_request/req = http_create_get("https://discordapp.com/api/channels/[id]/pins", headers = list("Authorization" = "Bot [token]")) req.begin_async() UNTIL(req.is_complete()) @@ -393,7 +393,7 @@ var/datum/discord_bot/discord_bot = null if (invite_url) return invite_url - var/datum/http_request/req = SShttp.get("https://discordapp.com/api/channels/[id]/invites", headers = list("Authorization" = "Bot [token]")) + var/datum/http_request/req = http_create_get("https://discordapp.com/api/channels/[id]/invites", headers = list("Authorization" = "Bot [token]")) req.begin_async() UNTIL(req.is_complete()) @@ -449,7 +449,7 @@ var/datum/discord_bot/discord_bot = null */ /datum/discord_channel/proc/create_invite(var/token) var/data = list("max_age" = 0, "max_uses" = 0) - var/datum/http_request/req = SShttp.post("https://discordapp.com/api/channels/[id]/invites", json_encode(data), list("Authorization" = "Bot [token]", "Content-Type" = "application/json")) + var/datum/http_request/req = http_create_post("https://discordapp.com/api/channels/[id]/invites", json_encode(data), list("Authorization" = "Bot [token]", "Content-Type" = "application/json")) req.begin_async() UNTIL(req.is_complete()) diff --git a/code/datums/discord/webhook.dm b/code/datums/discord/webhook.dm index 0481627cbf6..23e36493bb8 100644 --- a/code/datums/discord/webhook.dm +++ b/code/datums/discord/webhook.dm @@ -52,7 +52,7 @@ var/list/global_webhooks = list() else Data["content"] = "[mention]" - var/datum/http_request/req = SShttp.post(url, body = json_encode(Data), headers = list("Content-Type" = "application/json")) + var/datum/http_request/req = http_create_post(url, body = json_encode(Data), headers = list("Content-Type" = "application/json")) req.begin_async() UNTIL(req.is_complete()) diff --git a/code/game/machinery/newscaster.dm b/code/game/machinery/newscaster.dm index ffb831b3d2f..18ef8653d19 100644 --- a/code/game/machinery/newscaster.dm +++ b/code/game/machinery/newscaster.dm @@ -412,7 +412,7 @@ var/list/obj/machinery/newscaster/allCasters = list() //Global list that will co dat+="Return" send_theme_resources(human_or_robot_user) - human_or_robot_user << browse(enable_ui_theme(human_or_robot_user, dat), "window=newscaster_main;size=400x600") + human_or_robot_user << browse(enable_ui_theme(human_or_robot_user, dat), "window=newscaster_main;size=600x900") onclose(human_or_robot_user, "newscaster_main") /obj/machinery/newscaster/Topic(href, href_list) diff --git a/code/modules/http/forum_api.dm b/code/modules/http/forum_api.dm new file mode 100644 index 00000000000..73552d4842d --- /dev/null +++ b/code/modules/http/forum_api.dm @@ -0,0 +1,51 @@ +var/global/forum_api_key = null + +/datum/http_request/forum_api + var/end_point + +/datum/http_request/forum_api/New(ep) + end_point = ep + +/datum/http_request/forum_api/proc/_get_url(suffix) + PRIVATE_PROC(TRUE) + + . = "[config.forum_api_path]/[end_point]" + + if (suffix) + . += "/[suffix]" + + . += "?key=[global.forum_api_key]" + +/datum/http_request/forum_api/proc/prepare_get(subtopic, list/params) + var/url = _get_url(subtopic) + + if (length(params)) + url += "&[list2params(params)]" + + prepare(RUSTG_HTTP_METHOD_GET, url, null, null) + +/datum/http_request/forum_api/proc/prepare_post(subtopic, list/params) + prepare(RUSTG_HTTP_METHOD_POST, _get_url(subtopic), params2list(params), list("Content-Type" = "application/x-www-form-urlencoded")) + +/datum/http_request/forum_api/proc/prepare_delete(subtopic, list/params) + prepare(RUSTG_HTTP_METHOD_DELETE, _get_url(subtopic), params2list(params), list("Content-Type" = "application/x-www-form-urlencoded")) + +/datum/http_request/forum_api/into_response() + var/datum/http_response/R = ..() + + if (R.errored) + return R + + try + R.body = json_decode(R.body) + catch + R.errored = TRUE + R.error = "Malformed JSON returned." + return R + + var/list/resp_data = R.body + if (resp_data["errorCode"]) + R.errored = TRUE + R.error = resp_data["errorMessage"] + + return R diff --git a/code/controllers/subsystems/http.dm b/code/modules/http/http.dm similarity index 58% rename from code/controllers/subsystems/http.dm rename to code/modules/http/http.dm index 89b817abae4..459aa8cfe84 100644 --- a/code/controllers/subsystems/http.dm +++ b/code/modules/http/http.dm @@ -1,35 +1,26 @@ -var/datum/controller/subsystem/http/SShttp - -/datum/controller/subsystem/http - name = "HTTP" - flags = SS_NO_FIRE | SS_NO_INIT - -/datum/controller/subsystem/http/New() - NEW_SS_GLOBAL(SShttp) - -/datum/controller/subsystem/http/proc/request(method, url, body = "", list/headers) +/proc/http_create_request(method, url, body = "", list/headers) var/datum/http_request/R = new() R.prepare(method, url, body, headers) return R -/datum/controller/subsystem/http/proc/get(url, body = "", list/headers) - return request(RUSTG_HTTP_METHOD_GET, url, body, headers) +/proc/http_create_get(url, body = "", list/headers) + return http_create_request(RUSTG_HTTP_METHOD_GET, url, body, headers) -/datum/controller/subsystem/http/proc/post(url, body = "", list/headers) - return request(RUSTG_HTTP_METHOD_POST, url, body, headers) +/proc/http_create_post(url, body = "", list/headers) + return http_create_request(RUSTG_HTTP_METHOD_POST, url, body, headers) -/datum/controller/subsystem/http/proc/put(url, body = "", list/headers) - return request(RUSTG_HTTP_METHOD_PUT, url, body, headers) +/proc/http_create_put(url, body = "", list/headers) + return http_create_request(RUSTG_HTTP_METHOD_PUT, url, body, headers) -/datum/controller/subsystem/http/proc/delete(url, body = "", list/headers) - return request(RUSTG_HTTP_METHOD_DELETE, url, body, headers) +/proc/http_create_delete(url, body = "", list/headers) + return http_create_request(RUSTG_HTTP_METHOD_DELETE, url, body, headers) -/datum/controller/subsystem/http/proc/patch(url, body = "", list/headers) - return request(RUSTG_HTTP_METHOD_PATCH, url, body, headers) +/proc/http_create_patch(url, body = "", list/headers) + return http_create_request(RUSTG_HTTP_METHOD_PATCH, url, body, headers) -/datum/controller/subsystem/http/proc/head(url, body = "", list/headers) - return request(RUSTG_HTTP_METHOD_HEAD, url, body, headers) +/proc/http_create_head(url, body = "", list/headers) + return http_create_request(RUSTG_HTTP_METHOD_HEAD, url, body, headers) /datum/http_request var/id diff --git a/config/example/config.txt b/config/example/config.txt index 95467c4dd9e..025598b25a3 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -559,3 +559,18 @@ FAIL2TOPIC_ENABLED 0 #FAIL2TOPIC_MAX_FAILS 5 ## The name of the firewall rule used. #FAIL2TOPIC_RULE_NAME _dd_fail2topic + +### Forum API configuration +### Required for certain components of the game to function. +### Is compatible with the Invision Community API (specifically IPS4). +## The base URL for the forum. Do not include a training backslash! +#FORUM_API_PATH https://myforums.com/api +## The API key for the forum. +#FORUM_API_KEY abcdefgh + +### News subsystem configuration. +## Uncomment to enable the pulling of news from the forum API. +## See the section about configuration the forum API as well! +## Once enabled, make sure to write a news.json in the configuration +## folder as well. +#NEWS_USE_FORUM_API diff --git a/config/example/news.json b/config/example/news.json new file mode 100644 index 00000000000..b1c3a5dd080 --- /dev/null +++ b/config/example/news.json @@ -0,0 +1,8 @@ +{ + "_comment": "Remove all _comment lines if you put this into production!", + + "news_topics": { + "_comment": "news_topics is an object of topic IDs (as strings) to arrays of possible article authors.", + "1234": ["John Doe", "Jane Doe"] + } +} diff --git a/html/changelogs/skull132_forum-news.yml b/html/changelogs/skull132_forum-news.yml new file mode 100644 index 00000000000..6c6b3fe778f --- /dev/null +++ b/html/changelogs/skull132_forum-news.yml @@ -0,0 +1,5 @@ +author: Skull132 +delete-after: True + +changes: + - rscadd: "The game now pulls news from the forum threads. Showcasing the latest 10 posts per topic. Enjoy!"