Source: extensions/twitter/twitter.js

  1. /**
  2. * StreamRoller Copyright 2023 "SilenusTA https://www.twitch.tv/olddepressedgamer"
  3. *
  4. * StreamRoller is an all in one streaming solution designed to give a single
  5. * 'second monitor' control page and allow easy integration for configuring
  6. * content (ie. tweets linked to chat, overlays triggered by messages, hue lights
  7. * controlled by donations etc)
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as published
  11. * by the Free Software Foundation, either version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  21. */
  22. /**
  23. * @extension Twitter
  24. * Connects to twitter allowing sending of tweets. Initial version, needs expanded
  25. * functionality adding when I get time or someone requests it :P
  26. */
  27. // ############################# TWITTER.js ##############################
  28. // Allows posting and reading twitter
  29. // ---------------------------- creation --------------------------------------
  30. // Author: Silenus aka twitch.tv/OldDepressedGamer
  31. // GitHub: https://github.com/SilenusTA/StreamRoller
  32. // Date: 01-March-2022
  33. // ============================================================================
  34. // IMPORTS/VARIABLES
  35. // ============================================================================
  36. import * as fs from "fs";
  37. import { dirname } from "path";
  38. import { TwitterClient } from "twitter-api-client";
  39. import { fileURLToPath } from "url";
  40. import * as logger from "../../backend/data_center/modules/logger.js";
  41. import sr_api from "../../backend/data_center/public/streamroller-message-api.cjs";
  42. const __dirname = dirname(fileURLToPath(import.meta.url));
  43. const localConfig = {
  44. OUR_CHANNEL: "TWITTER_CHANNEL",
  45. EXTENSION_NAME: "twitter",
  46. SYSTEM_LOGGING_TAG: "[EXTENSION]",
  47. twitterClient: null,
  48. DataCenterSocket: null,
  49. channelConnectionAttempts: 20,
  50. heartBeatTimeout: 5000,
  51. heartBeatHandle: null,
  52. status: {
  53. connected: false // this is our connection indicator for discord
  54. },
  55. };
  56. let channelConnectionAttempts = 0;
  57. const default_serverConfig = {
  58. __version__: 0.2,
  59. extensionname: localConfig.EXTENSION_NAME,
  60. channel: localConfig.OUR_CHANNEL,
  61. twitterenabled: "off",
  62. //credentials variable names to use (in credentials modal)
  63. credentialscount: "4",
  64. cred1name: "twitterAPIkey",
  65. cred1value: "",
  66. cred2name: "twitterAPISecret",
  67. cred2value: "",
  68. cred3name: "twitterAccessToken",
  69. cred3value: "",
  70. cred4name: "TwitterAccessTokenSecret",
  71. cred4value: ""
  72. };
  73. let serverConfig = structuredClone(default_serverConfig);
  74. const triggersandactions =
  75. {
  76. extensionname: serverConfig.extensionname,
  77. description: "Send a tweet",
  78. version: "0.2",
  79. channel: serverConfig.channel,
  80. actions:
  81. [
  82. {
  83. name: "TwitterPostTweet",
  84. displaytitle: "Post a Tweet",
  85. description: "Post a message to twtter",
  86. messagetype: "action_PostTweet",
  87. parameters: {
  88. message: ""
  89. }
  90. }
  91. ],
  92. }
  93. // ============================================================================
  94. // FUNCTION: initialise
  95. // ============================================================================
  96. /**
  97. * Starts the extension using the given data.
  98. * @param {object:Express} app
  99. * @param {string} host
  100. * @param {number} port
  101. * @param {number} heartbeat
  102. */
  103. function initialise (app, host, port, heartbeat)
  104. {
  105. logger.extra(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "host", host, "port", port, "heartbeat", heartbeat);
  106. if (typeof (heartbeat) != "undefined")
  107. localConfig.heartBeatTimeout = heartbeat;
  108. else
  109. logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "DataCenterSocket no heatbeat passed:", heartbeat);
  110. try
  111. {
  112. localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect, onDataCenterDisconnect, host, port);
  113. } catch (err)
  114. {
  115. logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "DataCenterSocket connection failed:", err);
  116. }
  117. }
  118. // ============================================================================
  119. // FUNCTION: onDataCenterDisconnect
  120. // ============================================================================
  121. /**
  122. * Disconnection message sent from the server
  123. * @param {String} reason
  124. */
  125. function onDataCenterDisconnect (reason)
  126. {
  127. // do something here when disconnt happens if you want to handle them
  128. logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterDisconnect", reason);
  129. }
  130. // ============================================================================
  131. // FUNCTION: onDataCenterConnect
  132. // ============================================================================
  133. // Description: Received connect message
  134. // Parameters: socket
  135. /**
  136. * Connection message handler
  137. * @param {*} socket
  138. */
  139. function onDataCenterConnect (socket)
  140. {
  141. logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterConnect", "Creating our channel");
  142. sr_api.sendMessage(localConfig.DataCenterSocket,
  143. sr_api.ServerPacket("RequestConfig", serverConfig.extensionname));
  144. // Request our credentials from the server
  145. sr_api.sendMessage(localConfig.DataCenterSocket,
  146. sr_api.ServerPacket("RequestCredentials", serverConfig.extensionname));
  147. sr_api.sendMessage(localConfig.DataCenterSocket,
  148. sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel));
  149. localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
  150. }
  151. // ============================================================================
  152. // FUNCTION: onDataCenterMessage
  153. // ============================================================================
  154. /**
  155. * receives message from the socket
  156. * @param {data} server_packet
  157. */
  158. function onDataCenterMessage (server_packet)
  159. {
  160. if (server_packet.type === "ConfigFile")
  161. {
  162. if (server_packet.data != "" && server_packet.to === serverConfig.extensionname)
  163. {
  164. if (server_packet.data.__version__ != default_serverConfig.__version__)
  165. {
  166. serverConfig = structuredClone(default_serverConfig);
  167. console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile Updated", "The config file has been Updated to the latest version v" + default_serverConfig.__version__ + ". Your settings may have changed" + "\x1b[0m");
  168. }
  169. else
  170. serverConfig = structuredClone(server_packet.data);
  171. SaveConfigToServer();
  172. }
  173. }
  174. else if (server_packet.type === "CredentialsFile")
  175. {
  176. if (server_packet.to === serverConfig.extensionname && server_packet.data != "")
  177. // start twitter connection
  178. connectToTwitter(server_packet.data);
  179. else
  180. {
  181. if (serverConfig.twitterenabled == "on")
  182. logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage",
  183. serverConfig.extensionname + " CredentialsFile", "Credential file is empty make sure to set it on the admin page.");
  184. }
  185. }
  186. else if (server_packet.type === "ExtensionMessage")
  187. {
  188. let extension_packet = server_packet.data;
  189. if (extension_packet.type === "RequestSettingsWidgetSmallCode")
  190. SendSettingsWidgetSmall(extension_packet.from);
  191. else if (extension_packet.type === "RequestCredentialsModalsCode")
  192. SendCredentialsModal(extension_packet.from);
  193. else if (extension_packet.type === "SettingsWidgetSmallData")
  194. {
  195. // check that it was our modal
  196. if (extension_packet.data.extensionname === serverConfig.extensionname)
  197. {
  198. serverConfig.twitterenabled = "off";
  199. for (const [key, value] of Object.entries(extension_packet.data))
  200. serverConfig[key] = value;
  201. SaveConfigToServer();
  202. // broadcast our modal out so anyone showing it can update it
  203. SendSettingsWidgetSmall("");
  204. }
  205. }
  206. else if (extension_packet.type === "action_PostTweet")
  207. {
  208. // check this was sent to us
  209. if (extension_packet.to === serverConfig.extensionname)
  210. if (serverConfig.twitterenabled != "off")
  211. tweetmessage(extension_packet.data.message)
  212. else
  213. logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "tweeting disabled : ");
  214. }
  215. else if (extension_packet.type === "SendTriggerAndActions")
  216. {
  217. sr_api.sendMessage(localConfig.DataCenterSocket,
  218. sr_api.ServerPacket("ExtensionMessage",
  219. serverConfig.extensionname,
  220. sr_api.ExtensionPacket(
  221. "TriggerAndActions",
  222. serverConfig.extensionname,
  223. triggersandactions,
  224. "",
  225. server_packet.from
  226. ),
  227. "",
  228. server_packet.from
  229. )
  230. )
  231. }
  232. else if (extension_packet.type === "SettingsWidgetSmallCode"
  233. || extension_packet.type === "RequestSettingsWidgetLargeCode"
  234. || extension_packet.type === "SettingsWidgetLargeCode"
  235. || extension_packet.type === "TriggerAndActions")
  236. {
  237. // ignore these messages
  238. }
  239. else
  240. logger.warn(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage",
  241. "received Unhandled ExtensionMessage : ", server_packet);
  242. }
  243. else if (server_packet.type === "UnknownChannel")
  244. {
  245. if (channelConnectionAttempts++ < localConfig.channelConnectionAttempts)
  246. {
  247. logger.info(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "Channel " + server_packet.data + " doesn't exist, scheduling rejoin");
  248. setTimeout(() =>
  249. {
  250. sr_api.sendMessage(localConfig.DataCenterSocket,
  251. sr_api.ServerPacket(
  252. "JoinChannel", serverConfig.extensionname, server_packet.data
  253. ));
  254. }, 5000);
  255. }
  256. }
  257. // we have received data from a channel we are listening to
  258. else if (server_packet.type === "ChannelData")
  259. logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "received message from unhandled channel ", server_packet.dest_channel);
  260. else if (server_packet.type === "InvalidMessage")
  261. logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage",
  262. "InvalidMessage ", server_packet.data.error, server_packet);
  263. else if (server_packet.type === "ChannelJoined"
  264. || server_packet.type === "ChannelCreated"
  265. || server_packet.type === "ChannelLeft"
  266. || server_packet.type === "LoggingLevel"
  267. || server_packet.type === "ExtensionMessage"
  268. )
  269. {
  270. // just a blank handler for items we are not using to avoid message from the catchall
  271. }
  272. // ------------------------------------------------ unknown message type received -----------------------------------------------
  273. else
  274. logger.warn(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
  275. ".onDataCenterMessage", "Unhandled message type", server_packet.type);
  276. }
  277. // ============================================================================
  278. // FUNCTION: onDataCenterMessage
  279. // ============================================================================
  280. /**
  281. * Connects to the twitter API
  282. * @param {object} creds
  283. */
  284. function connectToTwitter (creds)
  285. {
  286. try
  287. {
  288. localConfig.twitterClient = new TwitterClient({
  289. apiKey: creds.twitterAPIkey,
  290. apiSecret: creds.twitterAPISecret,
  291. accessToken: creds.twitterAccessToken,
  292. accessTokenSecret: creds.TwitterAccessTokenSecret
  293. })
  294. localConfig.status.connected = true;
  295. /*
  296. localConfig.status.connected = false;
  297. logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".connectToTwitter", "twitter connection failed:", err, err.message);*/
  298. }
  299. catch (e)
  300. {
  301. localConfig.status.connected = false;
  302. logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "twitter connection failed:", e.message);
  303. }
  304. }
  305. // ===========================================================================
  306. // FUNCTION: SendSettingsWidgetSmall
  307. // ===========================================================================
  308. /**
  309. * send some modal code to be displayed on the admin page or somewhere else
  310. * this is done as part of the webpage request for modal message we get from
  311. * extension. It is a way of getting some user feedback via submitted forms
  312. * from a page that supports the modal system
  313. * @param {String} tochannel
  314. */
  315. function SendSettingsWidgetSmall (tochannel)
  316. {
  317. // read our modal file
  318. fs.readFile(__dirname + "/twittersettingswidgetsmall.html", function (err, filedata)
  319. {
  320. if (err)
  321. logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
  322. ".SendSettingsWidgetSmall", "failed to load modal", err);
  323. //throw err;
  324. else
  325. {
  326. let modalstring = filedata.toString();
  327. for (const [key, value] of Object.entries(serverConfig))
  328. {
  329. if (value === "on")
  330. modalstring = modalstring.replace(key + "checked", "checked");
  331. else if (typeof (value) == "string")
  332. modalstring = modalstring.replace(key + "text", value);
  333. }
  334. sr_api.sendMessage(localConfig.DataCenterSocket,
  335. sr_api.ServerPacket(
  336. "ExtensionMessage", // this type of message is just forwarded on to the extension
  337. serverConfig.extensionname,
  338. sr_api.ExtensionPacket(
  339. "SettingsWidgetSmallCode", // message type
  340. serverConfig.extensionname, //our name
  341. modalstring,// data
  342. "",
  343. tochannel,
  344. serverConfig.channel
  345. ),
  346. "",
  347. tochannel // in this case we only need the "to" channel as we will send only to the requester
  348. ))
  349. }
  350. });
  351. }
  352. // ===========================================================================
  353. // FUNCTION: SendCredentialsModal
  354. // ===========================================================================
  355. /**
  356. * Send our CredentialsModal to whoever requested it
  357. * @param {String} extensionname
  358. */
  359. function SendCredentialsModal (extensionname)
  360. {
  361. fs.readFile(__dirname + "/twittercredentialsmodal.html", function (err, filedata)
  362. {
  363. if (err)
  364. logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
  365. ".SendCredentialsModal", "failed to load modal", err);
  366. //throw err;
  367. else
  368. {
  369. let modalstring = filedata.toString();
  370. // first lets update our modal to the current settings
  371. for (const [key, value] of Object.entries(serverConfig))
  372. {
  373. // true values represent a checkbox so replace the "[key]checked" values with checked
  374. if (value === "on")
  375. {
  376. modalstring = modalstring.replace(key + "checked", "checked");
  377. } //value is a string then we need to replace the text
  378. else if (typeof (value) == "string")
  379. {
  380. modalstring = modalstring.replace(key + "text", value);
  381. }
  382. }
  383. // send the modal data to the server
  384. sr_api.sendMessage(localConfig.DataCenterSocket,
  385. sr_api.ServerPacket("ExtensionMessage",
  386. serverConfig.extensionname,
  387. sr_api.ExtensionPacket(
  388. "CredentialsModalCode",
  389. serverConfig.extensionname,
  390. modalstring,
  391. "",
  392. extensionname,
  393. serverConfig.channel
  394. ),
  395. "",
  396. extensionname)
  397. )
  398. }
  399. });
  400. }
  401. // ============================================================================
  402. // FUNCTION: SaveConfigToServer
  403. // ============================================================================
  404. /**
  405. * Sends our config to the server to be saved for next time we run
  406. */
  407. function SaveConfigToServer ()
  408. {
  409. // saves our serverConfig to the server so we can load it again next time we startup
  410. sr_api.sendMessage(localConfig.DataCenterSocket, sr_api.ServerPacket
  411. ("SaveConfig",
  412. serverConfig.extensionname,
  413. serverConfig))
  414. }
  415. // ============================================================================
  416. // Twitter client code
  417. // ============================================================================
  418. /**
  419. * tweet a message
  420. * @param {String} message
  421. */
  422. function tweetmessage (message)
  423. {
  424. try
  425. {
  426. localConfig.twitterClient.tweetsV2.createTweet({ "text": message })
  427. .then(response =>
  428. {
  429. logger.extra(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
  430. ".tweetmessage", "Tweet sent ", message);
  431. }).catch(err =>
  432. {
  433. localConfig.status.connected = false;
  434. logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
  435. ".tweetmessage", "Failed to tweet message ... ", err.name, err.message);
  436. })
  437. }
  438. catch (e)
  439. {
  440. logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
  441. ".tweetmessage", "Failed ... ", e.name, e.message);
  442. }
  443. }
  444. // ============================================================================
  445. // FUNCTION: heartBeat
  446. // ============================================================================
  447. /**
  448. * Sends out heartbeat messages so other extensions can see our status
  449. */
  450. function heartBeatCallback ()
  451. {
  452. let status = false;
  453. if (serverConfig.twitterenabled == "on" && localConfig.status.connected)
  454. status = true;
  455. sr_api.sendMessage(localConfig.DataCenterSocket,
  456. sr_api.ServerPacket("ChannelData",
  457. serverConfig.extensionname,
  458. sr_api.ExtensionPacket(
  459. "HeartBeat",
  460. serverConfig.extensionname,
  461. { connected: status },
  462. serverConfig.channel),
  463. serverConfig.channel
  464. ),
  465. );
  466. localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
  467. }
  468. // ============================================================================
  469. // EXPORTS
  470. // Note that initialise is mandatory to allow the server to start this extension
  471. // ============================================================================
  472. export { initialise, triggersandactions };