Advertisement
k1ck3r

twitch.lua

Nov 14th, 2017
1,033
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 14.38 KB | None | 0 0
  1. -- luacheck: globals ngx
  2. local ngx = ngx
  3.  
  4. local config = require'multistreamer.config'
  5. local encode_query_string = require('lapis.util').encode_query_string
  6. local encode_base64 = require('lapis.util.encoding').encode_base64
  7. local decode_base64 = require('lapis.util.encoding').decode_base64
  8. local encode_with_secret = require('lapis.util.encoding').encode_with_secret
  9. local decode_with_secret = require('lapis.util.encoding').decode_with_secret
  10. local to_json   = require('lapis.util').to_json
  11. local from_json = require('lapis.util').from_json
  12. local slugify = require('lapis.util').slugify
  13. local http = require'multistreamer.http'
  14. local resty_sha1 = require'resty.sha1'
  15. local str = require'resty.string'
  16. local string = require'multistreamer.string'
  17. local format = string.format
  18. local len = string.len
  19. local split = string.split
  20. local sub = string.sub
  21. local match = string.match
  22. local find = string.find
  23. local gsub = string.gsub
  24. local to_table = string.to_table
  25. local escape_markdown = string.escape_markdown
  26. local insert = table.insert
  27. local sort = table.sort
  28. local tonumber = tonumber
  29. local ngx_log = ngx.log
  30. local ngx_err = ngx.ERR
  31. local ngx_debug = ngx.DEBUG
  32. local sleep = ngx.sleep
  33.  
  34. local IRCClient = require'multistreamer.irc.client'
  35.  
  36. local Account = require'models.account'
  37. local StreamAccount = require'models.stream_account'
  38.  
  39. local M = {}
  40.  
  41. M.name = 'twitch'
  42. M.displayname = 'Twitch'
  43. M.allow_sharing = true
  44. M.read_comments = true
  45. M.write_comments = true
  46. M.redirect_uri = config.public_http_url .. config.http_prefix .. '/auth/twitch'
  47.  
  48. M.icon =
  49.   '<svg viewBox="0 0 16 16" xmlns="http://www.w3.or' ..
  50.   'g/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="rou' ..
  51.   'nd" stroke-miterlimit="1.414"><g fill-rule="nonzero"><path d="M1.393 0L.' ..
  52.   '35 2.783v11.13h3.824V16h2.088l2.085-2.088h3.13L15.65 9.74V0H1.394zm1.39 ' ..
  53.   '1.39H14.26v7.653l-2.435 2.435H8l-2.085 2.085v-2.085H2.783V1.39z"/><path ' ..
  54.   'd="M6.61 8.348H8V4.175H6.61v4.173zm3.824 0h1.39V4.175h-1.39v4.173z"/></g' ..
  55.   '></svg>'
  56.  
  57. local api_url = 'https://api.twitch.tv/kraken'
  58. local twitch_config = config.networks.twitch
  59.  
  60. local function http_error_handler(res)
  61.   return from_json(res.body).message
  62. end
  63.  
  64. local function twitch_api_client(access_token)
  65.   local httpc = http.new(http_error_handler)
  66.  
  67.   local _request = httpc.request
  68.  
  69.   httpc.request = function(self,method,endpoint,params,headers,body)
  70.     local url = api_url .. endpoint
  71.     local req_headers = {
  72.       ['Accept'] = 'application/vnd.twitchtv.v5+json',
  73.     }
  74.     if access_token then
  75.       req_headers['Authorization'] = 'OAuth ' .. access_token
  76.     end
  77.     if headers then
  78.       for k,v in pairs(headers) do
  79.           req_headers[k] = v
  80.       end
  81.     end
  82.  
  83.     local res, err = _request(self,method,url,params,req_headers,body)
  84.     if err then return false, err end
  85.     return from_json(res.body)
  86.   end
  87.  
  88.   httpc.get = function(self,endpoint,params,headers)
  89.     return httpc.request(self,'GET',endpoint,params,headers)
  90.   end
  91.   httpc.put = function(self,endpoint,params,headers)
  92.     return httpc.request(self,'PUT',endpoint,nil,headers,to_json(params))
  93.   end
  94.  
  95.   setmetatable(httpc,httpc)
  96.  
  97.   return httpc,nil
  98.  
  99. end
  100.  
  101. function M.metadata_fields()
  102.   return {
  103.     [1] = {
  104.         type = 'text',
  105.         label = 'Title',
  106.         key = 'title',
  107.         required = true,
  108.     },
  109.     [2] = {
  110.         type = 'text',
  111.         label = 'Game',
  112.         key = 'game',
  113.         required = true,
  114.     },
  115.   }
  116. end
  117.  
  118. function M.metadata_form(_, stream)
  119.  
  120.   local form = M.metadata_fields()
  121.   for _,k in ipairs(form) do
  122.     k.value = stream:get(k.key)
  123.   end
  124.  
  125.   return form, nil
  126. end
  127.  
  128.  
  129. function M.get_oauth_url(user, stream_id)
  130.   return format('%s/oauth2/authorize?',api_url)..
  131.          encode_query_string({
  132.            response_type = 'code',
  133.            force_verify = 'true',
  134.            redirect_uri = M.redirect_uri,
  135.            client_id = twitch_config.client_id,
  136.            state = encode_base64(encode_with_secret({ id = user.id, stream_id = stream_id })),
  137.            scope = 'user_read channel_read channel_editor channel_stream chat_login',
  138.          })
  139. end
  140.  
  141. function M.register_oauth(params)
  142.   if params.error_description then
  143.     return false, nil, 'Twitch Error: ' .. params.error_description
  144.   end
  145.   if params.error then
  146.     return false, nil, 'Twitch Error: ' .. params.error
  147.   end
  148.  
  149.   local user, _ = decode_with_secret(decode_base64(params.state))
  150.  
  151.   if not user then
  152.     return false, nil, 'Error: User not found'
  153.   end
  154.  
  155.   if not params.code then
  156.     return false, nil, 'Twitch Error: failed to get temporary client token'
  157.   end
  158.  
  159.   local httpc = http.new(http_error_handler)
  160.  
  161.   local body = encode_query_string({
  162.     client_id = twitch_config.client_id,
  163.     client_secret = twitch_config.client_secret,
  164.     redirect_uri = M.redirect_uri,
  165.     code = params.code,
  166.     state = params.state,
  167.     grant_type = 'authorization_code',
  168.   })
  169.  
  170.   local res, err = httpc:post(api_url .. '/oauth2/token', nil, nil, body)
  171.  
  172.   if err then return false, nil, err end
  173.  
  174.   local creds = from_json(res.body)
  175.  
  176.   local tclient = twitch_api_client(creds.access_token)
  177.   local user_info, user_err = tclient:get('/user')
  178.  
  179.   if user_err then
  180.     return false, nil, user_err
  181.   end
  182.  
  183.   local channel_info, channel_err = tclient:get('/channel')
  184.   if channel_err then
  185.     return false, nil, channel_err
  186.   end
  187.  
  188.   local sha1 = resty_sha1:new()
  189.   sha1:update(format('%d',user_info._id))
  190.  
  191.   local network_user_id = str.to_hex(sha1:final())
  192.  
  193.   local account = Account:find({
  194.     network = M.name,
  195.     network_user_id = network_user_id,
  196.   })
  197.  
  198.   if not account then
  199.     account = Account:create({
  200.       user_id = user.id,
  201.       network = M.name,
  202.       network_user_id = network_user_id,
  203.       name = user_info.display_name,
  204.       slug = slugify(user_info.name),
  205.     })
  206.   else
  207.    -- this account might be owned by somebody else
  208.     account:update({
  209.       name = user_info.display_name,
  210.       slug = slugify(user_info.name),
  211.     })
  212.   end
  213.  
  214.   -- since we may have just taken somebody's account, we'll update
  215.   -- the access token, channel etc anyway but return an error
  216.   account:set('token',creds.access_token)
  217.   account:set('channel',channel_info.name)
  218.   account:set('channel_id',channel_info._id)
  219.   account:set('stream_key',channel_info.stream_key)
  220.  
  221.   if(account.user_id ~= user.id) then
  222.     return false, nil, 'Error: Account already registered'
  223.   end
  224.  
  225.   local sa = nil
  226.   if user.stream_id then
  227.     sa = StreamAccount:find({ account_id = account.id, stream_id = user.stream_id })
  228.     if not sa then
  229.       sa = StreamAccount:create({ account_id = account.id, stream_id = user.stream_id })
  230.     end
  231.   end
  232.  
  233.   return account, sa, nil
  234. end
  235.  
  236. function M.publish_start(account, stream)
  237.   local stream_o = stream
  238.  
  239.   account = account:get_all()
  240.   stream = stream:get_all()
  241.  
  242.   local rtmp_url = gsub(twitch_config.ingest_server,'/+$','') .. '/'
  243.  
  244.   if account.stream_key then
  245.     rtmp_url = rtmp_url .. account.stream_key
  246.   else
  247.     return false, 'unable to create rtmp url'
  248.   end
  249.  
  250.   -- check required settings
  251.   if not stream.title or
  252.      len(stream.title) == 0 or
  253.      not stream.game or
  254.      len(stream.game) == 0 then
  255.     return false, 'Twitch: missing required settings'
  256.   end
  257.  
  258.   local tclient = twitch_api_client(account.token)
  259.   local res, err = tclient:put('/channels/'..account.channel_id, {
  260.     channel = {
  261.       status = stream.title,
  262.       game = stream.game,
  263.     }
  264.   }, {
  265.     ['Content-Type'] = 'application/json'
  266.   })
  267.  
  268.   if not res then
  269.     if type(err) == 'table' then
  270.       return false, err.error
  271.     else
  272.       return false, err
  273.     end
  274.   end
  275.  
  276.   stream_o:set('http_url','https://twitch.tv/' .. account.channel)
  277.   stream_o:set('channel',account.channel)
  278.   return rtmp_url, nil
  279. end
  280.  
  281. function M.publish_stop(_, stream)
  282.   stream:unset('http_url')
  283.  
  284.   return true
  285. end
  286.  
  287. function M.check_errors(account)
  288.   local token = account:get('token')
  289.  
  290.   if not token then return 'No OAuth token' end
  291.  
  292.   local channel_id = account:get('channel_id')
  293.  
  294.   if not channel_id then
  295.     local tclient = twitch_api_client(token)
  296.     local channel_info, err = tclient:get('/channel')
  297.     if err then
  298.       return err
  299.     end
  300.     account:set('channel_id', channel_info._id)
  301.   end
  302.  
  303.   return false
  304. end
  305.  
  306. function M.notify_update(_,_)
  307.   return true
  308. end
  309.  
  310. local function linkify(token)
  311.   if not token then return '' end
  312.   if find(token,"^https?://") then
  313.     return format('[%s](%s)',token,token)
  314.   else
  315.     return escape_markdown(token)
  316.   end
  317. end
  318.  
  319. local function emojify(message,emotes)
  320.   local msgTable = to_table(message)
  321.   if not emotes then
  322.     emotes = {}
  323.   else
  324.     emotes = split(emotes,'/')
  325.   end
  326.   for _,v in ipairs(emotes) do
  327.     local t = find(v,':')
  328.     if t then
  329.       local emote = sub(v,1,t-1)
  330.       local ranges = split(sub(v,t+1),',')
  331.       for _,r in ipairs(ranges) do
  332.         local b,e = match(r,'(%d+)-(%d+)')
  333.         b = tonumber(b) + 1
  334.         e = tonumber(e) + 1
  335.         local alt_text = sub(message,b,e)
  336.         for i=b,e,1 do
  337.           msgTable[i] = nil
  338.         end
  339.         msgTable[b] = format('![%s](http://static-cdn.jtvnw.net/emoticons/v1/%s/1.0)',alt_text,emote)
  340.       end
  341.     end
  342.   end
  343.  
  344.   local keys = {}
  345.  
  346.   for k,_ in pairs(msgTable) do
  347.     insert(keys,k)
  348.   end
  349.  
  350.   sort(keys)
  351.  
  352.   local cur_token = ''
  353.   local outmsg = ''
  354.  
  355.   for _,k in ipairs(keys) do
  356.     local text = msgTable[k]
  357.     if len(text) > 1 then
  358.       if len(cur_token) > 0 then
  359.         outmsg = outmsg .. linkify(cur_token)
  360.         cur_token = ''
  361.       end
  362.       outmsg = outmsg .. text
  363.     else
  364.       if text == ' ' then
  365.         outmsg = outmsg .. linkify(cur_token) .. ' '
  366.         cur_token = ''
  367.       else
  368.         cur_token = cur_token .. text
  369.       end
  370.     end
  371.   end
  372.  
  373.   if len(cur_token) > 0 then
  374.     outmsg = outmsg .. linkify(cur_token)
  375.   end
  376.   return outmsg
  377. end
  378.  
  379. function M.create_viewcount_func(account, _, send)
  380.   if not send then return nil end
  381.  
  382.   local tclient = twitch_api_client(account['token'])
  383.   local viewRunning = true
  384.   local viewcount_func, stop_viewcount_func
  385.  
  386.   viewcount_func = function()
  387.     while viewRunning do
  388.       local res, err = tclient:get('/streams/' .. account['channel_id'])
  389.       if not err then
  390.         if type(res.stream) == 'table' then
  391.           send({viewer_count = tonumber(res.stream.viewers)})
  392.         end
  393.       end
  394.       sleep(60)
  395.     end
  396.     return true
  397.   end
  398.  
  399.   stop_viewcount_func = function()
  400.     viewRunning = false
  401.   end
  402.  
  403.   return viewcount_func, stop_viewcount_func
  404.  
  405. end
  406.  
  407. function M.create_comment_funcs(account, stream, send)
  408.   local irc = IRCClient.new()
  409.   local nick = account.channel:lower()
  410.   local channel = '#' .. stream.channel:lower()
  411.   local icons = {}
  412.   local tclient = twitch_api_client()
  413.   local my_user_id
  414.  
  415.   local function irc_connect()
  416.     local ok, err
  417.     ngx_log(ngx_debug,format('[%s] IRC: Connecting',M.displayname))
  418.     ok, err = irc:connect('irc.chat.twitch.tv',6667)
  419.     if not ok then
  420.       ngx_log(ngx_err,format('[%s] IRC: Connection failed: %s',M.displayname,err))
  421.       return false,err
  422.     end
  423.     ngx_log(ngx_debug,format('[%s] IRC: logging in as %s',M.displayname,nick))
  424.     ok, err = irc:login(nick,nil,nil,'oauth:'..account.token)
  425.     if not ok then
  426.       ngx_log(ngx_err,format('[%s] IRC: Login for "%s" failed: %s',M.displayname,nick,err))
  427.       return false,err
  428.     end
  429.     ngx_log(ngx_debug,format('[%s] IRC: logged in as %s',M.displayname,nick))
  430.     irc:join(channel)
  431.     irc:capreq('twitch.tv/tags')
  432.     irc:capreq('twitch.tv/commands')
  433.     return true, nil
  434.   end
  435.  
  436.   local function getIcon(userid)
  437.     if icons[userid] == nil then
  438.       local icon_res = tclient:get('/users/' .. userid, nil, {
  439.         ['Client-ID'] = twitch_config.client_id,
  440.  
  441.       })
  442.       if icon_res then
  443.         icons[userid] = icon_res.logo
  444.       end
  445.     end
  446.     return icons[userid]
  447.   end
  448.  
  449.   if send then
  450.     local tclient_temp = twitch_api_client(account['token'])
  451.     local user_info = tclient_temp:get('/user/')
  452.     my_user_id = user_info._id
  453.     icons[my_user_id] = user_info.logo
  454.   end
  455.  
  456.   local function sendMsg(event,data)
  457.     local msg = {
  458.       from = {
  459.         name = data.tags['display-name'],
  460.         id = data.tags['user-id'],
  461.       },
  462.       text = data.message,
  463.       markdown = emojify(data.message,data.tags.emotes),
  464.     }
  465.     if len(msg.from.name) == 0 then
  466.       msg.from.name = data.from.nick
  467.     end
  468.  
  469.     msg.from.picture = getIcon(msg.from.id)
  470.  
  471.     if data.to == nick then
  472.       msg.to = {
  473.         name = nick,
  474.       }
  475.     end
  476.  
  477.     ngx_log(ngx_debug,format('[%s] IRC: Received message from %s',M.displayname,msg.from.name))
  478.  
  479.     if event == 'message' then
  480.       msg.type = 'text'
  481.     elseif event == 'emote' then
  482.       msg.type = 'emote'
  483.     end
  484.     send(msg)
  485.   end
  486.  
  487.   local irc_ok, irc_err = irc_connect()
  488.   if not irc_ok then return false, irc_err end
  489.  
  490.   local running = true
  491.  
  492.   local stop_func = function()
  493.     running = false
  494.     irc:quit()
  495.   end
  496.  
  497.   local read_func = function()
  498.     if send then
  499.       irc:onEvent('message',sendMsg)
  500.       irc:onEvent('emote',sendMsg)
  501.     end
  502.     while running do
  503.       local cruise_ok, cruise_err = irc:cruise()
  504.  
  505.       if not running then
  506.         ngx_log(ngx_debug,format('[%s] IRC Client ending normally for %s',M.displayname,nick))
  507.         return true
  508.       end
  509.  
  510.       if not cruise_ok then
  511.         ngx_log(ngx_err,format('[%s] IRC Client error: %s, reconnecting',M.displayname,cruise_err))
  512.       end
  513.  
  514.       local reconnect_ok, reconnect_err = irc_connect()
  515.  
  516.       if not reconnect_ok then
  517.         ngx_log(ngx_err,format('[%s] IRC Client error: %s, giving up',M.displayname,reconnect_err))
  518.         return false, reconnect_err
  519.       end
  520.     end
  521.     return true
  522.   end
  523.  
  524.   local write_func = function(message)
  525.     if message.type == 'text' then
  526.       irc:message(channel,message.text)
  527.     elseif message.type == 'emote' then
  528.       irc:emote(channel,message.text)
  529.     end
  530.     -- we don't get messages echo'd back
  531.     -- from IRC, so we'll echo on our own here
  532.     if send then
  533.       local msg = {
  534.         from = {
  535.           name = account.channel,
  536.           picture = getIcon(my_user_id),
  537.         },
  538.         text = message.text,
  539.         markdown = escape_markdown(message.text),
  540.         type = message.type,
  541.       }
  542.       send(msg)
  543.     end
  544.   end
  545.  
  546.   return read_func, write_func, stop_func
  547. end
  548.  
  549. return M
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement