diff --git a/ByondPOST.dll b/ByondPOST.dll new file mode 100644 index 00000000000..b33f70b1ec8 Binary files /dev/null and b/ByondPOST.dll differ diff --git a/baystation12.dme b/baystation12.dme index e46880cadf2..f1a7f10e973 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -130,6 +130,7 @@ #include "code\datums\crew.dm" #include "code\datums\datacore.dm" #include "code\datums\datumvars.dm" +#include "code\datums\discord_bot.dm" #include "code\datums\disease.dm" #include "code\datums\mind.dm" #include "code\datums\mixed.dm" @@ -1056,7 +1057,6 @@ #include "code\modules\examine\descriptions\structures.dm" #include "code\modules\examine\descriptions\turfs.dm" #include "code\modules\examine\descriptions\weapons.dm" -#include "code\modules\ext_scripts\discord.dm" #include "code\modules\ext_scripts\python.dm" #include "code\modules\flufftext\Dreaming.dm" #include "code\modules\flufftext\Hallucination.dm" @@ -1067,6 +1067,7 @@ #include "code\modules\holodeck\HolodeckControl.dm" #include "code\modules\holodeck\HolodeckObjects.dm" #include "code\modules\holodeck\HolodeckPrograms.dm" +#include "code\modules\http\post_request.dm" #include "code\modules\hydroponics\_hydro_setup.dm" #include "code\modules\hydroponics\grown.dm" #include "code\modules\hydroponics\grown_inedible.dm" diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index 99cdc085998..944b29fcb3e 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -807,6 +807,22 @@ var/list/gamemode_cache = list() age_restrictions += name age_restrictions[name] = text2num(value) + else if (type == "discord") + // Ideally, this would never happen. But just in case. + if (!discord_bot) + log_debug("BOREALIS: Attempted to read config/discord.txt before initializing the bot.") + return + + switch (name) + if ("token") + discord_bot.auth_token = value + if ("active") + discord_bot.active = 1 + if ("robust_debug") + discord_bot.robust_debug = 1 + else + log_misc("Unknown setting in discord configuration: '[name]'") + /datum/configuration/proc/pick_mode(mode_name) // I wish I didn't have to instance the game modes in order to look up // their information, but it is the only way (at least that I know of). diff --git a/code/datums/api.dm b/code/datums/api.dm index 3a73955cadb..c3cf798d94d 100644 --- a/code/datums/api.dm +++ b/code/datums/api.dm @@ -632,6 +632,7 @@ proc/api_update_command_database() s["players"] = 0 s["stationtime"] = worldtime2text() s["roundduration"] = round_duration() + s["gameid"] = game_id if(queryparams["status"] == "2") var/list/players = list() @@ -645,9 +646,9 @@ proc/api_update_command_database() players += C.key s["players"] = players.len - s["playerlist"] = list2params(players) + s["playerlist"] = players s["admins"] = admins.len - s["adminlist"] = list2params(admins) + s["adminlist"] = admins else var/n = 0 var/admins = 0 @@ -974,3 +975,29 @@ proc/api_update_command_database() else qdel(P) return 2 + +// Update discord_bot's channels. +/datum/topic_command/update_bot_channels + name = "update_bot_channels" + description = "Tells the ingame instance of the Discord bot to update its cached channels list." + +/datum/topic_command/update_bot_channels/run_command() + data = null + + if (!discord_bot) + statuscode = 404 + response = "Ingame Discord bot not initialized." + return 1 + + switch (discord_bot.update_channels()) + if (1) + statuscode = 404 + response = "Ingame Discord bot is not active." + if (2) + statuscode = 500 + response = "Ingame Discord bot encountered error attempting to access database." + else + statuscode = 200 + response = "Ingame Discord bot's channels were successfully updated." + + return 1 diff --git a/code/datums/discord_bot.dm b/code/datums/discord_bot.dm new file mode 100644 index 00000000000..1238e3e93e4 --- /dev/null +++ b/code/datums/discord_bot.dm @@ -0,0 +1,155 @@ +#define CHAN_ADMIN "channel_admin" +#define CHAN_CCIAA "channel_cciaa" +#define CHAN_ANNOUNCE "channel_announce" + +var/datum/discord_bot/discord_bot = null + +/hook/startup/proc/initialize_discord_bot() + if (discord_bot) + // This shouldn't be possible, but sure! + return 0 + + discord_bot = new() + + config.load("config/discord.txt", "discord") + + discord_bot.update_channels() + + return 1 + +/datum/discord_bot + var/list/channels = list() + + var/active = 0 + var/auth_token = "" + + var/robust_debug = 0 + + // Lazy man's rate limiting vars + var/rate_limited_since = 0 + var/queue_being_pushed = 0 + var/list/queue = list() + +/datum/discord_bot/proc/update_channels() + if (!active) + return 1 + + if (!establish_db_connection(dbcon)) + log_debug("BOREALIS: Failed to update channels due to missing database.") + return 2 + + channels = list() + + var/DBQuery/channel_query = dbcon.NewQuery("SELECT channel_group, channel_id FROM discord_channels") + channel_query.Execute() + + var/list/A + while (channel_query.NextRow()) + if (isnull(channels[channel_query.item[1]])) + channels[channel_query.item[1]] = list() + + A = channels[channel_query.item[1]] + A += channel_query.item[2] + + log_debug("BOREALIS: Channels updated successfully.") + return 0 + +/datum/discord_bot/proc/send_message(var/channel_group, var/message) + if (!active || !auth_token) + return + + if (!channel_group || !channels.len || isnull(channels[channel_group])) + return + + if (!message) + return + + if (length(message) > 2000) + message = copytext(message, 1, 2001) + + // Let's run it through the proper JSON encoder, just in case of special characters. + message = json_encode(list("content" = message)) + + var/list/A = channels[channel_group] + var/list/sent = list() + for (var/channel in A) + if (send_post_request("https://discordapp.com/api/channels/[channel]/messages", message, "Authorization: Bot [auth_token]", "Content-Type: application/json") == 429) + // Whoopsies, rate limited. + // Set up the queue. + rate_limited_since = world.time + queue.Add(list(message, A - sent)) + + // Schedule a push. + spawn (100) + push_queue() + + // And exit. + return + else + sent += channel + + if (robust_debug) + log_debug("BOEALIS: Message sent to [channel_group]. JSON body: '[message]'") + +/datum/discord_bot/proc/send_to_admins(message) + send_message(CHAN_ADMIN, message) + +/datum/discord_bot/proc/send_to_cciaa(message) + send_message(CHAN_CCIAA, message) + +/datum/discord_bot/proc/send_to_announce(message) + send_message(CHAN_ANNOUNCE, message) + +/datum/discord_bot/proc/push_queue() + // What facking queue. + if (!queue.len) + if (robust_debug) + log_debug("BOREALIS: Attempted to push a null length queue.") + if (queue_being_pushed) + queue_being_pushed = 0 + return + + if (queue_being_pushed) + if (robust_debug) + log_debug("BOREALIS: Attempted to initialize a second queue driver.") + return + + if ((world.time - rate_limited_since) < 100) + // Something broke the limit again. Ideally, this wouldn't happen. But sure. + // Use a longer timeout, just in case. + spawn (200) + push_queue() + + queue_being_pushed = 0 + return + + // Async process lock var. No touchy. + queue_being_pushed = 1 + + // A[1] - message body. + // A[2] - list of channels to send to. + var/message + var/list/destinations + for (var/list/A in queue) + message = A[1] + destinations = A[2] + + for (var/channel in destinations) + if (send_post_request("https://discordapp.com/api/channels/[channel]/messages", message, "Authorization: Bot [auth_token]", "Content-Type: application/json") == 429) + // Limited again. Reschedule. + rate_limited_since = world.time + spawn (100) + push_queue() + + queue_being_pushed = 0 + return + else + destinations.Remove(channel) + + queue.Remove(A) + + queue_being_pushed = 0 + +#undef CHAN_ADMIN +#undef CHAN_CCIAA +#undef CHAN_ANNOUNCE diff --git a/code/game/antagonist/antagonist_print.dm b/code/game/antagonist/antagonist_print.dm index 31a5b96b2ce..756071003be 100644 --- a/code/game/antagonist/antagonist_print.dm +++ b/code/game/antagonist/antagonist_print.dm @@ -83,4 +83,29 @@ if(purchases) text += "
[purchases]" - return text \ No newline at end of file + return text + +/datum/antagonist/proc/print_player_summary_discord() + if (current_antagonists.len) + return "" + + var/text = "[current_antagonists.len > 1 ? "The [lowertext(role_text_plural)] were:\n" : "The [lowertext(role_text)] was:\n"]" + for (var/datum/mind/ply in current_antagonists) + var/role = ply.assigned_role ? "\improper[ply.assigned_role]" : "\improper[ply.special_role]: " + text += "**[ply.name]** (**[ply.key]**) as \a **[role]** (" + if(ply.current) + if(ply.current.stat == DEAD) + text += "died" + else if(isNotStationLevel(ply.current.z)) + text += "fled the station" + else + text += "survived" + if(ply.current.real_name != ply.name) + text += " as **[ply.current.real_name]**" + else + text += "body destroyed" + text += ")\n" + + text += "\n" + + return text diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm index 523157df59b..c226d64f2cf 100644 --- a/code/game/gamemodes/game_mode.dm +++ b/code/game/gamemodes/game_mode.dm @@ -369,14 +369,22 @@ var/global/list/additional_antag_types = list() /datum/game_mode/proc/declare_completion() var/is_antag_mode = (antag_templates && antag_templates.len) + var/discord_text = "A round of **[name]** has ended! \[Game ID: [game_id]\]\n\n" check_victory() if(is_antag_mode) sleep(10) - for(var/datum/antagonist/antag in antag_templates) + for (var/datum/antagonist/antag in antag_templates) sleep(10) antag.check_victory() antag.print_player_summary() + // Avoid the longest loop if we aren't actively using the bot. + if (discord_bot.active) + discord_text += antag.print_player_summary_discord() + + discord_bot.send_to_announce(discord_text) + discord_text = "" + var/clients = 0 var/surviving_humans = 0 var/surviving_total = 0 @@ -423,10 +431,17 @@ var/global/list/additional_antag_types = list() if(surviving_total > 0) text += "
There [surviving_total>1 ? "were [surviving_total] survivors" : "was one survivor"]" text += " ([escaped_total>0 ? escaped_total : "none"] [emergency_shuttle.evac ? "escaped" : "transferred"]) and [ghosts] ghosts.
" + + discord_text += "There [surviving_total>1 ? "were **[surviving_total] survivors**" : "was **one survivor**"]" + discord_text += " ([escaped_total>0 ? escaped_total : "none"] [emergency_shuttle.evac ? "escaped" : "transferred"]) and **[ghosts] ghosts**." else text += "There were no survivors ([ghosts] ghosts)." + + discord_text += "There were **no survivors** ([ghosts] ghosts)." world << text + discord_bot.send_to_announce(discord_text) + if(clients > 0) feedback_set("round_end_clients",clients) if(ghosts > 0) diff --git a/code/game/gamemodes/gameticker.dm b/code/game/gamemodes/gameticker.dm index ce99b333e30..fe16c4251ee 100644 --- a/code/game/gamemodes/gameticker.dm +++ b/code/game/gamemodes/gameticker.dm @@ -147,7 +147,7 @@ var/global/datum/controller/gameticker/ticker if(C.holder && (C.holder.rights & (R_MOD|R_ADMIN))) admins_number++ if(admins_number == 0) - send_to_admin_discord("@everyone Round has started with no admins online.") + discord_bot.send_to_admins("@here Round has started with no admins online.") /* supply_controller.process() //Start the supply shuttle regenerating points -- TLE // handled in scheduler master_controller.process() //Start master_controller.process() diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 94fc6a7c496..4d85f3b0a75 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -99,10 +99,12 @@ var/list/adminhelp_ignored_words = list("unknown","the","a","an","of","monkey"," msg = "\blue Request for Help:: [get_options_bar(mob, 3, 1, 1)][ai_cl]: [msg]" + var/admin_number_present = 0 var/admin_number_afk = 0 for(var/client/X in admins) if((R_ADMIN|R_MOD) & X.holder.rights) + admin_number_present++ if(X.is_afk()) admin_number_afk++ if(X.prefs.toggles & SOUND_ADMINHELP) @@ -113,9 +115,9 @@ var/list/adminhelp_ignored_words = list("unknown","the","a","an","of","monkey"," //show it to the person adminhelping too src << "PM to-Staff : [original_msg]" - var/admin_number_present = admins.len - admin_number_afk + var/admin_number_active = admin_number_present - admin_number_afk log_admin("HELP: [key_name(src)]: [original_msg] - heard by [admin_number_present] non-AFK admins.") - if(admin_number_present <= 0) - send_to_admin_discord("@everyone Request for Help from [key_name(src)]: [html_decode(original_msg)] - !![admin_number_afk ? "All admins AFK ([admin_number_afk])" : "No admins online"]!!") + if(admin_number_active <= 0) + discord_bot.send_to_admins("@everyone Request for Help from [key_name(src)]: [html_decode(original_msg)] - !![admin_number_afk ? "All admins AFK ([admin_number_afk])" : "No admins online"]!!") feedback_add_details("admin_verb","AH") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! return diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index a75aacd7999..24635707c92 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -129,7 +129,7 @@ sanitize(msg) - send_to_admin_discord("PlayerPM to [sender] from [key_name(src)]: [html_decode(msg)]") + discord_bot.send_to_admins("PlayerPM to [sender] from [key_name(src)]: [html_decode(msg)]") src << "" + create_text_tag("pm_out_alt", "", src) + " to Discord-[sender]: [msg]" diff --git a/code/modules/admin/verbs/modifyvariables.dm b/code/modules/admin/verbs/modifyvariables.dm index 4ed1684a213..74fe4bc0457 100644 --- a/code/modules/admin/verbs/modifyvariables.dm +++ b/code/modules/admin/verbs/modifyvariables.dm @@ -1,7 +1,8 @@ var/list/forbidden_varedit_object_types = list( /datum/admins, //Admins editing their own admin-power object? Yup, sounds like a good idea. /obj/machinery/blackbox_recorder, //Prevents people messing with feedback gathering - /datum/feedback_variable //Prevents people messing with feedback gathering + /datum/feedback_variable, //Prevents people messing with feedback gathering + /datum/discord_bot //Nope.jpg. Stop it. ) /* diff --git a/code/modules/admin/verbs/pray.dm b/code/modules/admin/verbs/pray.dm index 04c1c02ccd4..05485f11bce 100644 --- a/code/modules/admin/verbs/pray.dm +++ b/code/modules/admin/verbs/pray.dm @@ -29,7 +29,7 @@ //log_admin("HELP: [key_name(src)]: [msg]") /proc/Centcomm_announce(var/msg, var/mob/Sender, var/iamessage) - send_to_cciaa_discord("!!! @everyone - Emergency message from the station: `[msg]`, sent by [Sender] !!!") + discord_bot.send_to_cciaa("@here - Emergency message from the station: `[msg]`, sent by [Sender]!") var/msg_cciaa = "\blue CENTCOMM[iamessage ? " IA" : ""]:[key_name(Sender, 1)] (RPLY): [msg]" msg = "\blue CENTCOMM[iamessage ? " IA" : ""]:[key_name(Sender, 1)] (PP) (VV) (SM) (JMP) (CA) (BSA) (RPLY): [msg]" diff --git a/code/modules/http/post_request.dm b/code/modules/http/post_request.dm new file mode 100644 index 00000000000..ffc04a1d872 --- /dev/null +++ b/code/modules/http/post_request.dm @@ -0,0 +1,80 @@ +/* + + @===================================@ + | | + | Guide to HTTP Post requests | + | | + @===================================@ + + Making POST requests in byond is SUPER easy with the post request DLL. + + Simply use the call() function to call the post request DLL and enter your details! + + The first bit of code needed will always be the same, You will never have to touch this, Copy-pasta all you like: + + call("ByondPOST.dll", "send_post_request") + + Well thats the first part, the harder part is to input the details into DLL. Lets do that now! The syntax to add arguments to the post request is: + + call("ByondPOST.dll", "send_post_request")(PostURL, PostContent, Header) + + In a lot of cases you might need more then one custom header to make a valid POST request, One such case is when you want to use the Discord API, + the discord API requires 2 custom headers, one to tell the server that you are making a JSON post request, + the header for this is "Content-Type: application/json" And another to tell the Discord API your login token, + Which looks like this "Authorization: YourLoginToken" + This can be achieved in Byond with the following code + + call("ByondPOST.dll", "send_post_request")("http://example.com", somebodyhere, "Content-Type: application/json", "Authorization: YourTokenHere") + + As you can see we have added some more arguments onto the proc, You can add as many arguments you like to the proc, Any argument after the PostContent + argument is considered a header. + + Some example POST requests: + + <-- Send Discord Message --> + call("ByondPOST.dll", "send_post_request")("https://discordapp.com/api/channels/134720091576205312/messages", " { \"content\" : \"Hello World!\" } ", "Content-Type: application/json", "Authorization: DAsDAs4!"�DFdW45%fAsFSa^$!"�$Xfdsfh523ds") + + DLL Written by Oisin100 and modified by Skull132 +*/ + +/* + * A generic proc for sending a post request with the aforementioned .DLL files. + * Expected arg structure: + * 1st arg - the url + * 2nd arg - the request body + * 3rd - nth arg - individual headers and their values in format: "headername: value" + * + * @return int - Error code from one of three possible sources! + * -1 indicates proc or library failure. + * 0 - 92 are curl errors, and are usually accompanied by a HTTP response code of 0 (request was never made). + * 100 - 6xx are HTTP response codes. Curl error code should be 0 in this case, but, in case that it is not, + * the HTTP response code is always returned as long as it is not 0. + * + */ +/proc/send_post_request() + if (args.len < 2) + return -1 + + var/result = call("ByondPOST.dll", "send_post_request")(arglist(args)) + + if (!result) + log_debug("ByondPOST: No result returned from external library.") + return -1 + + var/list/A = params2list(result) + + if (!isnull(A["proc"])) + // Log the proc error. It should be reviewed by coders ASAP. + switch (A["proc"]) + if ("1") + log_debug("ByondPOST: Proc error: Too few arguments sent to function.") + if ("2") + log_debug("ByondPOST: Proc error: Unable to initialize curl object.") + else + log_debug("ByondPOST: Proc error: Unknown error.") + return -1 + + // Curl oriented errors should leave the HTTP response code at 0, as no request was executed. + // All HTTP oriented errors will definately return a response code other than 0, so prioritize that. + // Fallback is a curl error code (0 - 92). + return text2num(A["http"]) != 0 ? text2num(A["http"]) : text2num(A["curl"]) diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index 72f484f36c4..71ccf5a52b7 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -4,11 +4,20 @@ log_access("Logout: [key_name(src)]") if(admin_datums[src.ckey]) if (ticker && ticker.current_state == GAME_STATE_PLAYING) //Only report this stuff if we are currently playing. - var/admins_number = admins.len + var/admins_number = 0 + var/admins_number_afk = 0 + for (var/client/C) + if (C.holder && (C.holder.rights & (R_MOD|R_ADMIN))) + admins_number++ + if (C.is_afk()) + admins_number_afk++ message_admins("Admin logout: [key_name(src)]") - if(admins_number == 0) //Apparently the admin logging out is no longer an admin at this point, so we have to check this towards 0 and not towards 1. Awell. - send_to_admin_discord("@everyone [key_name(src)] logged out - no more admins online.") + + if (admins_number == 0) //Apparently the admin logging out is no longer an admin at this point, so we have to check this towards 0 and not towards 1. Awell. + discord_bot.send_to_admins("@here [key_name(src)] logged out - no more admins online.") + else if ((admins_number - admins_number_afk) <= 0) + discord_bot.send_to_admins("[key_name(src)] logged out - only AFK admins ([admins_number_afk]) are online.") ..() return 1 diff --git a/code/modules/paperwork/faxmachine.dm b/code/modules/paperwork/faxmachine.dm index 326ae34dac4..75f7dcf85fd 100644 --- a/code/modules/paperwork/faxmachine.dm +++ b/code/modules/paperwork/faxmachine.dm @@ -252,7 +252,7 @@ var/list/sent_faxes = list() //cache for faxes that have been sent by the admins if((R_ADMIN|R_CCIAA) & C.holder.rights) C << msg - send_to_cciaa_discord("New fax arrived! [faxname]: \"[sent.name]\" by [sender].") + discord_bot.send_to_cciaa("New fax arrived! [faxname]: \"[sent.name]\" by [sender].") /obj/machinery/photocopier/faxmachine/proc/do_pda_alerts() if (!alert_pdas || !alert_pdas.len) diff --git a/config/example/discord.txt b/config/example/discord.txt new file mode 100644 index 00000000000..c7725c4f755 --- /dev/null +++ b/config/example/discord.txt @@ -0,0 +1,9 @@ +## Uncomment this to activate the DM controlled Discord bot. +# ACTIVE + +## Paste the bot's OAuth2 token as a value to this setting. Mind the caps! +# TOKEN + +## Uncomment this to enable robust debugging. +## This results in more messages being sent via log_debug() during bot operations. +# ROBUST_DEBUG diff --git a/html/changelogs/skull132-BOREALISII.yml b/html/changelogs/skull132-BOREALISII.yml new file mode 100644 index 00000000000..5ce40c0bd82 --- /dev/null +++ b/html/changelogs/skull132-BOREALISII.yml @@ -0,0 +1,6 @@ +author: Skull132 + +delete-after: True + +changes: + - rscadd: "Implemented BOREALIS II into the game. Updates and other information from the game will now be transmitted to both the public and private Discords, as necessary." diff --git a/lib/ByondPostDLL/ByondPostDLL.cpp b/lib/ByondPostDLL/ByondPostDLL.cpp new file mode 100644 index 00000000000..9a79d934529 --- /dev/null +++ b/lib/ByondPostDLL/ByondPostDLL.cpp @@ -0,0 +1,70 @@ +/* + Copyright (C) 2016 Oisin Carr & Skull132 + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include + +extern "C" __declspec(dllexport) char *send_post_request(int argc, char *argv[]) +{ + if (argc < 2) + { + return "proc=1"; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + CURL *curl = curl_easy_init(); + + if (!curl) + { + return "proc=2"; + } + + // Initialize variables. + static char return_value[32]; + long http_code = 0; + CURLcode res; + struct curl_slist *chunk = NULL; + + for (int i = 2; i < argc; i++) + { + chunk = curl_slist_append(chunk, argv[i]); + } + + // Set curl options. + curl_easy_setopt(curl, CURLOPT_URL, argv[0]); + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + + char *data = argv[1]; + + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data); + + // Save the response. + res = curl_easy_perform(curl); + + // Get the response code and save it to http_code. + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + // Clean up the session info. + curl_global_cleanup(); + + // Create the feedback message. + // Format used is key=value&key=value. + snprintf(return_value, 32, "http=%d&curl=%d", http_code, res); + + return return_value; +} diff --git a/libcurl.dll b/libcurl.dll new file mode 100644 index 00000000000..1da2bd26768 Binary files /dev/null and b/libcurl.dll differ