Skip to content
This repository has been archived by the owner on Jan 19, 2021. It is now read-only.

Writing Network Modules

John Regan edited this page Feb 19, 2017 · 14 revisions

Network modules are just Lua tables with some predefined methods.

There's two kinds of modules - oauth, and non-oauth.

An OAuth module will expose a method to create the service's OAuth registration URL, and a method for creating an account after the user's hit "Authorize" on the external service.

A non-OAuth module exposes a method to create a simple registration form, the module can request and save any pieces of data it needs. The main app will handle saving all the pieces of data.

There's two skeleton network modules to serve as a starting point, networks/skeleton.oauth.lua and networks/skeleton.plain.lua

All modules expose methods for:

  • Creating a metadata-editing form (multistreamer handles saving the data).
  • Updating any needed metadata and returning an RTMP URL when the user starts pushing video.

All modules also have a displayname field.

OAuth modules will have a redirect_uri field set by Multistreamer.

Most modules will begin with something like:

local config = require('lapis.config').get()
local encode_base64 = require('lapis.util.encoding').encode_base64
local decode_base64 = require('lapis.util.encoding').decode_base64
local encode_with_secret = require('lapis.util.encoding').encode_with_secret
local decode_with_secret = require('lapis.util.encoding').decode_with_secret
local Account = require'models.account'
local resty_sha1 = require'resty.sha1'
local str = require 'resty.string'
local http = require'resty.http'

local module = {}

module.displayname = 'Demo Module'

-- methods...

return module

module.displayname = string (all modules)

The "display name" to be shown in the web UI.

module.allow_sharing = boolean (all modules)

If a network module should allow accounts to be shared with other users, this should be true. Right now, Facebook is the only account that doesn't allow sharing.

url = module.get_oauth_url(user) (oauth network modules only)

user is a User object

You'll need to make use of the redirect_uri field. Additionally, OAuth services have some method of including a piece of data, you'll need to include the user.id field in order to look up the user later.

function module.get_oauth_url(user)
  local encoded_user = encode_base64(encode_with_secret({ id = user.id }))
  return 'https://some/service?redirect_uri=' .. module.redirect_uri ..
         '&state=' .. encoded_user
end

account, err = module.register_oauth(params) (oauth network modules only)

params will be the query parameters from the OAuth app.

The module will then register one Account with the Account model. The network module is responsible for checking that the Account is unique before saving. All accounts have a hex-encoded sha1 network_user_id field.

Individual Account objects have a keystore for saving bits of data, such as OAuth Access Tokens, and anything else you may need.

function M.register_oauth(params)
  local user, err = decode_with_secret(decode_base64(params.state))
  local httpc = http.new()

  -- make some requests, get the user id from your service
  local sha1 = resty_sha1:new()
  sha1:update(some-external-user-id)

  local network_user_id = str.to_hex(sha1:final())
  local some_user_name = -- somehow get a username to display

  local account = Account:find({
    network = module.name,
    network_user_id = network_user_id,
  })

  if not account then
    account = Account:create({
      user_id = user.id,
      network = module.name,
      network_user_id = network_user_id,
      name = some_user_name
    })
  else
    -- account already exists!
    -- check if this account belongs to the
    -- user, etc
  end

  account:set('some-field','something')

  return account , nil
end

table = module.create_form() (non-oauth network modules)

For non-oauth modules, this will return a table describing a form for adding a new account, or for editing an existing account.

This form is also used for verifying required data has been entered.

The documentation for module.metadata_form (further down) has more details on what the returned table should look like.

function module.create_form()
  return {
    [1] = {
      type = 'text',
      label = 'Name',
      key = 'name',
      required = true,
    },
    [2] = {
      type = 'text',
      label = 'URL',
      key = 'url',
      required = true,
    },
  }
end

account, err = module.save_account(user, account, params) (non-oauth network modules)

This is the function for saving an account.

  • user - the authenticated user object
  • account - account if this is an existing account, nil otherwise
  • params - the params from the account creation form

The module is responsible for generating some type of unique identifier for the account, when account is nil. If account is defined, the module can just update the account.

function module.save_account(user,account,params)
  local account = account
  local err

  local sha1 = resty_sha1:new()
  sha1:update(params.something-that-should-be-unique)
  local key = str.to_hex(sha1:final())

  if not account then
    -- double-check that an account isn't being duplicated
    account, err = Account:find({
      network = M.network,
      network_user_id = key,
    })
  end

  if not account then
    -- looks like this is a new account
    account, err = Account:create({
      network = M.name,
      network_user_id = key,
      name = params.name,
      user_id = user.id
    })
    if not account then
        return false,err
    end
  else
    -- either account was already provided, or found with Account:find
    account:update({
      name = params.name,
    })
  end

  account:set('something',params.something-you-care-about)

  return account, nil
end

table = module.metadata_form(account,stream) (all network modules)

This function returns a table describing a form for per-stream settings. The account and stream parameters are keystores for the user's account and stream, respectively. The account keystore should be used for retrieving account-wide settings (OAuth access tokens, etc), while the stream keystore is for per-stream settings, like the stream title.

Each entry in the table needs a type, label,key field. You can include a value field to include a pre-filled value.

When the user submits the form, Multistreamer will save each of the keys to the stream keystore.

Supported field types:

  • text - generate a regular, single-line text field.
  • textarea - generates a multiline text area
  • select - generate a dropdown

The select field type requires another field - options, which should be a table of objects, like:

options = { { value = 1, label = 'first' }, { value = 2, label = 'second' } }

function module.metadata_form(account, stream)
  local oauth_token = account:get('oauth-token')

  -- make some http requests, do some things, etc
  return {
    [1] = {
        type = 'text',
        label = 'Title',
        key = 'title',
        value = stream:get('title'),
    },
    [2] = {
        type = 'text',
        label = 'Game',
        key = 'game',
        value = stream:get('title'),
    },
    [3] = {
        type = 'select',
        label = 'Mature',
        key = 'mature',
        value = stream:get('mature'),
        options = {
            { value = 0, label = 'No' },
            { value = 1, label = 'Yes' },
        },
    },
  }
end

table = module.metadata_fields() (all modules)

This function should return a table describing the required and optional per-stream settings. This is used by multistreamer to actually save the keys from the metadata_form function.

If the module doesn't require/accept any per-stream settings, just return something false-y.

All that's needed for each table entry is a key field and an optional required field. All other fields are ignored.

function module.metadata_fields()
  return {
    [1] = {
        key = 'title',
        required = true,
    },
    [2] = {
        key = 'game',
        required = true,
    },
    [3] = {
        key = 'hashtags',
        required = false,
    },
  }
end

rtmp_url, err = module.publish_start(account, stream) (all network modules)

This function is called when the user starts streaming data. account and stream are keystores to account-wide settings and per-stream settings.

The returned function should take whatever actions it needs to create a video, update channel metadata, and get or generate an RTMP URL.

If available, it should also save a shareable HTTP URL to the stream keystore as http_url - this URL is used for sending out notifications/tweets/etc.

Some example code

function module.publish_start(account,stream)
  local title = stream:get('title')

  -- do something with title, like make an http request to the service
  local rtmp_url = make_rtmp_url_somehow()
  return rtmp_url, nil
end

ok, err = module.notify_update(account, stream) (all network modules)

This function is called every 30 seconds while a stream is active.

If you need to make some type of request after streaming has started, this is where you'd do it. For example, the YouTube module has to 'transition' a live broadcast to 'live' after video has started, so it's done within this module.

If streaming needs to be stopped for some reason, return something false-y.

function module.notify_update(account, stream)
  local stream_id = stream:get('stream_id')

  -- do some things
  return true, nil
end

nil = module.publish_stop(account, stream) (all network modules)

This function is called when the user stops streaming data. account and stream are keystores to account-wide settings and per-stream settings.

function module.publish_stop(account,stream)
  stream:unset('something')
  return
end

read_func, write_func = module.create_comment_funcs(account, stream, send)

When a stream goes live, this function is called to create read and write functions stream comments.

account and stream are per-account and per-stream tables (they're keystores in table form), send is a function that should be called for every new comment/chat/etc on the stream.

The returned read_func will be spawned into an nginx thread - it should loop indefinitely. Whenever it has a new comment, it should call send with a table like:

type = 'text', -- can be 'text' or 'emote'
text = 'message', -- only used with type='text'
markdown = 'markdown-message,' -- markdown variant of message (optional)
from = { name = 'name', id = 'id' }

The write_func will be called as-needed to post comments/chat messages to the video stream. It should accept a single parameter - a table with the keys type and text - type can be text (for posting a standard comment/message) or emote for performing some kind of emote action.

Example:

function module.create_comment_funcs(account,stream,send)

  local http_client = create_http_client(account['access_token'])
  local stream_id = stream['stream_id']

  local read_func = function()
    while true do
      local res, err = http_client:get_comments(stream_id)
      if res then
        for k,v in pairs(res) do
          send({
            type = 'text',
            text = v.message,
            markdown = v.markdown,
            from = {
              name = v.from.name,
              id = v.from.id,
            },
          })
        end
      end
      ngx.sleep(10)
    end
  end

  local write_func = function(msg)
    if msg.type == 'text' then
      http_client:post_comment(stream_id,msg.text)
    elseif mgs.type == 'emote' them
      http_client:post_emote(stream_id,msg.text)
    end
  end

  return read_func, write_func
end

view_func = module.create_viewcount_func(account, stream, send)

When a stream goes live, this function is called to create a function for getting the stream's viewer count.

account and stream are per-account and per-stream tables (they're keystores in table form), send is a function that should be called whenever the view count has updated.

The returned view_func will be spawned into an nginx thread - it should loop indefinitely. Whenever it has an updated viewer count, it should call send with a table like:

viewer_count = 10 -- the current viewer count

Example:

function module.create_viewcount_func(account,stream,send)

  local http_client = create_http_client(account['access_token'])
  local stream_id = stream['stream_id']

  local viewcount_func = function()
    while true do
      local res, err = http_client:get_viewcount(stream_id)
      if res then
        send({ viewer_count = res.viewcount })
      end
      ngx.sleep(10)
    end
  end

  return viewcount_func
end

err = module.check_errors(account) (all network modules)

This function is called everytime the user pulls up the main index page. It provides an opportunity to check if any keys are out-of-date, refresh metadata, etc.

It should either return some error text to display next to the account, or something false-y/nil if there's nothing to display.

Clone this wiki locally