TwitchPubSug.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import WS from "ws"
  2. import translateTwitchUser from "./utils/translateTwitchUser";
  3. import { EventEmitter } from "events"
  4. import { runInThisContext } from "vm";
  5. export interface PointsRedeemed {
  6. timestamp: string;
  7. redemption: {
  8. id: string,
  9. user: {
  10. id: string,
  11. login: string,
  12. display_name: string
  13. },
  14. channel_id: string,
  15. redeemed_at: string,
  16. reward: {
  17. id: string,
  18. channel_id: string,
  19. title: string,
  20. prompt: string,
  21. cost: number,
  22. is_user_input_required: boolean,
  23. is_sub_only: boolean
  24. }
  25. user_input: string,
  26. status: string
  27. }
  28. }
  29. export interface ChannelPointsEvent {
  30. type: "reward-redeemed",
  31. data: PointsRedeemed
  32. }
  33. export interface ResponseEvent {
  34. type: "RESPONSE",
  35. error: string,
  36. nonce?: string
  37. }
  38. export interface MesageEvent {
  39. type: "MESSAGE",
  40. data: {
  41. message: string,
  42. topic: string
  43. }
  44. }
  45. export interface PongEvent {
  46. type: "PONG"
  47. }
  48. export type TwitchEvent = MesageEvent | ResponseEvent | PongEvent
  49. declare interface Twitch {
  50. on(event: "reward", listner: (reward: PointsRedeemed)=>void): this;
  51. on(event: "error", listner: (msg: string)=>void): this;
  52. on(event: "start", listner: ()=>void): this;
  53. emit(event: "reward", reward: PointsRedeemed): boolean;
  54. emit(event: "error", msg: string): boolean;
  55. emit(event: "start"): boolean;
  56. }
  57. class Twitch extends EventEmitter{
  58. stats = {
  59. pingCounter: 0,
  60. pongCounter: 0
  61. }
  62. ws?: WS;
  63. private pingInterval: NodeJS.Timeout;
  64. private interval?: NodeJS.Timeout;
  65. private inputs: {
  66. StartStopButton?: HTMLButtonElement,
  67. ErrStatus?: HTMLElement
  68. } = {};
  69. constructor(){
  70. super();
  71. }
  72. async spawn(): Promise<WS>{
  73. let translateID:string = "";
  74. try{
  75. translateID = await translateTwitchUser(window.settings.options.channel);
  76. }catch(e){
  77. this.emit("error", "Can't translate username to id");
  78. }
  79. if(translateID == ""){
  80. throw new Error(`There is no user with nickname ${window.settings.options.channel}`);
  81. }
  82. let ws = new WS("wss://pubsub-edge.twitch.tv");
  83. this.pingInterval = setInterval(()=>{
  84. ws.send(JSON.stringify({
  85. "type": "PING"
  86. }));
  87. this.stats.pingCounter++;
  88. }, 1000*60*5)
  89. let nonce = "ascdas89412casl";
  90. ws.on("open", ()=>{
  91. console.log("Opened websocket");
  92. this.updateElement();
  93. ws.send(JSON.stringify({
  94. "type": "LISTEN",
  95. "nonce": nonce,
  96. "data": {
  97. "topics": ["channel-points-channel-v1."+translateID],
  98. "auth_token": window.settings.options.twitch_oauth_token
  99. }
  100. }));
  101. })
  102. ws.on("message", (data)=>{
  103. console.log("Data:");
  104. let dataObj = JSON.parse(data.toString()) as TwitchEvent;
  105. console.log(dataObj);
  106. if(dataObj.type == "RESPONSE"){
  107. if(dataObj.error != ""){
  108. this.emit("error", dataObj.error);
  109. this.stop();
  110. }
  111. }else if(dataObj.type == "MESSAGE"){
  112. let message = JSON.parse(dataObj.data.message) as ChannelPointsEvent;
  113. if(message.type == "reward-redeemed"){
  114. this.emit("reward", message.data);
  115. }
  116. }else if(dataObj.type == "PONG"){
  117. this.stats.pongCounter++;
  118. }else{
  119. console.log("Unsupported event");
  120. }
  121. })
  122. ws.on("close", ()=>{
  123. clearInterval(this.pingInterval)
  124. this.pingInterval = undefined;
  125. this.updateElement();
  126. })
  127. return ws;
  128. }
  129. start(){
  130. if(!this.ws){
  131. this.spawn().then(ws => {
  132. this.emit("start");
  133. this.ws = ws;
  134. }).catch(e => {
  135. this.emit("error", e);
  136. });
  137. }
  138. }
  139. stop(){
  140. if(this.ws){
  141. this.ws.close();
  142. this.ws = undefined;
  143. }
  144. }
  145. status(): boolean{
  146. if(this.ws){
  147. return this.ws.readyState == this.ws.OPEN
  148. }
  149. return false;
  150. }
  151. bind(
  152. StaStoBtn: HTMLButtonElement,
  153. ErrStatus: HTMLElement
  154. ){
  155. this.inputs.StartStopButton = StaStoBtn;
  156. this.inputs.ErrStatus = ErrStatus;
  157. this.on("error", (msg)=>{
  158. this.inputs.ErrStatus.innerText = msg;
  159. })
  160. this.on("start", ()=>{
  161. this.inputs.ErrStatus.innerText = "";
  162. })
  163. this.inputs.StartStopButton.addEventListener("click", e => {
  164. console.log("Start stop");
  165. if(this.status()){
  166. this.stop();
  167. }else{
  168. this.start();
  169. }
  170. })
  171. let refresher = () => this.updateElement();
  172. this.interval = setInterval(refresher, 1000);
  173. setTimeout(refresher, 0);
  174. }
  175. updateElement(){
  176. let status = document.getElementById("twitchStatus");
  177. if(this.status()){
  178. status.innerText = "Online";
  179. status.style.color = "Green";
  180. if(this.inputs.StartStopButton)
  181. this.inputs.StartStopButton.innerText = "Stop";
  182. }else{
  183. status.innerText = "Offline";
  184. status.style.color = "red";
  185. if(this.inputs.StartStopButton)
  186. this.inputs.StartStopButton.innerText = "Start";
  187. }
  188. }
  189. }
  190. export default Twitch;