From ec774a8fd389f78100a4397ebf55042c6614f37f Mon Sep 17 00:00:00 2001 From: John Regan Date: Sun, 19 Aug 2018 14:58:13 -0500 Subject: [PATCH] New feature: import/export stream settings as JSON (see #47) --- lib/multistreamer/api/v1.lua | 185 ++---------------- lib/multistreamer/importexport.lua | 181 +++++++++++++++++ lib/multistreamer/lang/en_us.lua | 6 + lib/multistreamer/version.lua | 6 +- lib/multistreamer/views/stream-advanced.etlua | 10 + lib/multistreamer/views/stream-menu.etlua | 2 - lib/multistreamer/webapp.lua | 42 ++++ rockspecs/multistreamer-12.2.0-0.rockspec | 96 +++++++++ rockspecs/multistreamer-template.rockspec | 1 + 9 files changed, 355 insertions(+), 174 deletions(-) create mode 100644 lib/multistreamer/importexport.lua create mode 100644 rockspecs/multistreamer-12.2.0-0.rockspec diff --git a/lib/multistreamer/api/v1.lua b/lib/multistreamer/api/v1.lua index 2f43e81..fd213d9 100644 --- a/lib/multistreamer/api/v1.lua +++ b/lib/multistreamer/api/v1.lua @@ -24,6 +24,7 @@ local StreamAccount = require'multistreamer.models.stream_account' local SharedStream = require'multistreamer.models.shared_stream' local SharedAccount = require'multistreamer.models.shared_account' local Webhook = require'multistreamer.models.webhook' +local importexport = require'multistreamer.importexport' local streams_dict = ngx.shared.streams local pid = ngx.worker.pid() @@ -45,76 +46,28 @@ local function get_stream(self, id) if not stream then return { status = 400, json = { error = 'unauthorized to access that stream' } } end - local chat_level, meta_level = stream:user_prep(self.user) - local stream_status = streams_dict:get(stream.id) - if stream_status then - stream_status = from_json(stream_status) - else - stream_status = { data_pushing = false } - end - if stream_status.data_pushing == true then - stream.live = true - else - stream.live = false - end - stream.webhooks = stream:get_webhooks() - for _,w in pairs(stream.webhooks) do - w.events = from_json(w.params).events - w.params = nil - end - if chat_level > 0 or meta_level > 0 then - - return { json = { stream = stream } } - end - return { status = 400, json = { error = 'unauthorized to access that stream' } } + return importexport.export_stream(self, stream) end + local ret = {} + local streams = self.user:get_streams() for _,v in ipairs(streams) do - v:user_prep(self.user) - - local stream_status = streams_dict:get(v.id) - if stream_status then - stream_status = from_json(stream_status) - else - stream_status = { data_pushing = false } - end - if stream_status.data_pushing == true then - v.live = true - else - v.live = false - end - v.webhooks = v:get_webhooks() - for _,w in pairs(v.webhooks) do - w.events = from_json(w.params).events - w.params = nil + local export = importexport.export_stream(self,v) + if export.status == 200 then + insert(ret,export.json.stream) end end + local sas = self.user:get_shared_streams() for _,v in ipairs(sas) do local s = v:get_stream() - s:user_prep(self.user) - - local stream_status = streams_dict:get(s.id) - if stream_status then - stream_status = from_json(stream_status) - else - stream_status = { data_pushing = false } - end - if stream_status.data_pushing == true then - s.live = true - else - s.live = false - end - s.webhooks = s:get_webhooks() - for _,w in pairs(s.webhooks) do - w.events = from_json(w.params).events - w.params = nil + local export = importexport.export_stream(self,s) + if export.status == 200 then + insert(ret,export.json.stream) end - - insert(streams,s) end - return { json = { streams = streams } } + return { json = { streams = ret } } end local function get_account(self, id) @@ -459,120 +412,14 @@ app:match('api-v1-stream',api_prefix .. '/stream(/:id)', respond_to({ if not stream then return { status = 400, json = { error = 'name needed when making new stream' } } end - self.params.name = stream.name - end - - self.params.stream_name = self.params.name - local og_accounts = self.params.accounts - local og_shares = self.params.shares - local og_webhooks = self.params.webhooks - self.params.webhooks = nil - - if og_accounts then - self.params.accounts = {} - - for _,acc in ipairs(self.user:get_accounts()) do - self.params.accounts[acc.id] = false - end - for _, sa in ipairs(self.user:get_shared_accounts()) do - self.params.accounts[sa.account_id] = false - end - - for _,v in ipairs(og_accounts) do - if v.id then - self.params.accounts[v.id] = true - end - end - end - - stream, err = Stream:save_stream(self.user,stream,self.params) - - if not stream then - return { status = 400, json = { error = err } } - end - - if self.params.title then - stream:set('title',self.params.title) - end - if self.params.description then - stream:set('description',self.params.description) - end - - if og_accounts then - for _,acc in ipairs(og_accounts) do - local account = Account:find({ id = acc.id }) - - if not account then - return { status = 400, json = { error = 'unauthorized to use that account' } } - end - if not account:check_user(self.user) then - return { status = 400, json = { error = 'unauthorized to use that account' } } - end - - local sa = StreamAccount:find({ account_id = account.id, stream_id = stream.id }) - local metadata_fields = networks[account.network].metadata_fields() - if acc.ffmpeg_args and acc.ffmpeg_args ~= cjson.null then - sa:update({ ffmpeg_args = acc.ffmpeg_args }) - elseif acc.ffmpeg_args == cjson.null then - sa:update({ ffmpeg_args = db.NULL }) - end - - for _,f in pairs(metadata_fields) do - if f.required and not acc.settings or not acc.settings[f.key] then - return { status = 400, json = { error = 'missing required field ' .. f.key } } - end - if acc.settings[f.key] then - sa:set(f.key,acc.settings[f.key]) - else - sa:unset(f.key) - end - end - end end - if og_shares then - if og_shares == cjson.null then - for _,v in ipairs(stream:get_stream_shares()) do - v:delete() - end - else - for _,v in ipairs(og_shares) do - local ss = SharedStream:find({ stream_id = stream.id, user_id = v.user.id }) - if not ss then - SharedStream:create({ - stream_id = stream.id, - user_id = v.user.id, - chat_level = v.chat_level, - metadata_level = v.metadata_level, - }) - else - ss:update({ - chat_level = v.chat_level, - metadata_level = v.metadata_level, - }) - end - end - end - end + local ret = importexport.import_stream(self, stream, self.params) - if og_webhooks then - for _,w in pairs(stream:get_webhooks()) do - w:delete() - end - if og_webhooks ~= cjson.null then - for _,w in ipairs(og_webhooks) do - local t = {} - t.stream_id = stream.id - t.url = w.url - t.events = w.events - t.notes = w.notes - Webhook:create(t) - end - end + if ret.status == 200 then + publish('stream:update',stream) end - - publish('stream:update',stream) - return get_stream(self, stream.id) + return ret end), })) diff --git a/lib/multistreamer/importexport.lua b/lib/multistreamer/importexport.lua new file mode 100644 index 0000000..d2e0737 --- /dev/null +++ b/lib/multistreamer/importexport.lua @@ -0,0 +1,181 @@ +-- luacheck: globals ngx networks +local ngx = ngx + +local to_json = require('lapis.util').to_json +local from_json = require('lapis.util').from_json +local cjson = require'cjson' +local db = require'lapis.db' + +local User = require'multistreamer.models.user' +local Account = require'multistreamer.models.account' +local Stream = require'multistreamer.models.stream' +local StreamAccount = require'multistreamer.models.stream_account' +local SharedStream = require'multistreamer.models.shared_stream' +local SharedAccount = require'multistreamer.models.shared_account' +local Webhook = require'multistreamer.models.webhook' + +local streams_dict = ngx.shared.streams + +local insert = table.insert +local pairs = pairs +local ipairs = ipairs + +local function export_stream(self, stream) + local chat_level, meta_level = stream:user_prep(self.user) + local stream_status = streams_dict:get(stream.id) + if stream_status then + stream_status = from_json(stream_status) + else + stream_status = { data_pushing = false } + end + if stream_status.data_pushing == true then + stream.live = true + else + stream.live = false + end + + stream.webhooks = stream:get_webhooks() + for _,w in pairs(stream.webhooks) do + w.events = from_json(w.params).events + w.params = nil + end + if chat_level > 0 or meta_level > 0 then + return { status = 200, json = { stream = stream } } + end + return { status = 400, json = { error = 'unauthorized to access that stream' } } +end + +local function import_stream(self, stream, params) + if stream then + local meta_level = stream:check_meta(self.user) + if meta_level < 2 then + return { status = 401, json = { error = 'unauthorized to edit stream' } } + end + end + + if not params.name then + params.stream_name = stream.name + else + params.stream_name = params.name + end + + local og_accounts = params.accounts + local og_shares = params.shares + local og_webhooks = params.webhooks + params.webhooks = nil + + if og_accounts then + params.accounts = {} + + for _,acc in ipairs(self.user:get_accounts()) do + params.accounts[acc.id] = false + end + for _, sa in ipairs(self.user:get_shared_accounts()) do + params.accounts[sa.account_id] = false + end + + for _,v in ipairs(og_accounts) do + if v.id then + params.accounts[v.id] = true + end + end + end + + stream, err = Stream:save_stream(self.user,stream,params) + + if not stream then + return { status = 400, json = { error = err } } + end + + if params.title then + stream:set('title',params.title) + end + if params.description then + stream:set('description',params.description) + end + + if og_accounts then + for _,acc in ipairs(og_accounts) do + local account = Account:find({ id = acc.id }) + + if not account then + return { status = 400, json = { error = 'unauthorized to use that account' } } + end + if not account:check_user(self.user) then + return { status = 400, json = { error = 'unauthorized to use that account' } } + end + + local sa = StreamAccount:find({ account_id = account.id, stream_id = stream.id }) + if not sa then + sa = StreamAccount:create({ account_id = account.id, stream_id = stream.id }) + end + + local metadata_fields = networks[account.network].metadata_fields() + if acc.ffmpeg_args and acc.ffmpeg_args ~= cjson.null then + sa:update({ ffmpeg_args = acc.ffmpeg_args }) + elseif acc.ffmpeg_args == cjson.null then + sa:update({ ffmpeg_args = db.NULL }) + end + + for _,f in pairs(metadata_fields) do + if f.required and not acc.settings or not acc.settings[f.key] then + return { status = 400, json = { error = 'missing required field ' .. f.key } } + end + if acc.settings[f.key] then + sa:set(f.key,acc.settings[f.key]) + else + sa:unset(f.key) + end + end + end + end + + if og_shares then + if og_shares == cjson.null then + for _,v in ipairs(stream:get_stream_shares()) do + v:delete() + end + else + for _,v in ipairs(og_shares) do + local ss = SharedStream:find({ stream_id = stream.id, user_id = v.user.id }) + if not ss then + SharedStream:create({ + stream_id = stream.id, + user_id = v.user.id, + chat_level = v.chat_level, + metadata_level = v.metadata_level, + }) + else + ss:update({ + chat_level = v.chat_level, + metadata_level = v.metadata_level, + }) + end + end + end + end + + if og_webhooks then + for _,w in pairs(stream:get_webhooks()) do + w:delete() + end + if og_webhooks ~= cjson.null then + for _,w in ipairs(og_webhooks) do + local t = {} + t.stream_id = stream.id + t.url = w.url + t.events = w.events + t.notes = w.notes + t['type'] = w['type'] + Webhook:create(t) + end + end + end + + return export_stream(self, stream) +end + +return { + import_stream = import_stream, + export_stream = export_stream, +} diff --git a/lib/multistreamer/lang/en_us.lua b/lib/multistreamer/lang/en_us.lua index 9a1eca5..8888fce 100644 --- a/lib/multistreamer/lang/en_us.lua +++ b/lib/multistreamer/lang/en_us.lua @@ -242,4 +242,10 @@ return { restart_error = 'Unrecoverable error! Please check logs and restart', stream_create_error = 'Failed to create stream: %s', stream_update_error = 'Failed to update stream: %s', + export = 'Export', + export_long = 'Export settings as JSON', + import = 'Import', + import_long = 'Import settings from JSON', + stream_import_success = 'Successfully imported stream settings', + stream_import_error = 'Error importing stream settings', } diff --git a/lib/multistreamer/version.lua b/lib/multistreamer/version.lua index ffd1307..051cffc 100644 --- a/lib/multistreamer/version.lua +++ b/lib/multistreamer/version.lua @@ -1,6 +1,6 @@ return { MAJOR = 12, - MINOR = 1, - PATCH = 1, - STRING = '12.1.1', + MINOR = 2, + PATCH = 0, + STRING = '12.2.0', } diff --git a/lib/multistreamer/views/stream-advanced.etlua b/lib/multistreamer/views/stream-advanced.etlua index 2341acb..17b57f2 100644 --- a/lib/multistreamer/views/stream-advanced.etlua +++ b/lib/multistreamer/views/stream-advanced.etlua @@ -11,6 +11,16 @@ <% end %> +
+ + +
+ +
+ + + +
diff --git a/lib/multistreamer/views/stream-menu.etlua b/lib/multistreamer/views/stream-menu.etlua index 5e43a8b..19e285d 100644 --- a/lib/multistreamer/views/stream-menu.etlua +++ b/lib/multistreamer/views/stream-menu.etlua @@ -5,9 +5,7 @@
  • <%= config.lang.permissions %>
  • <%= config.lang.webhooks %>
  • <%= config.lang.general %>
  • - <% if config.allow_custom_puller then %>
  • <%= config.lang.advanced %>
  • - <% end %>

    diff --git a/lib/multistreamer/webapp.lua b/lib/multistreamer/webapp.lua index 885c522..0342a21 100644 --- a/lib/multistreamer/webapp.lua +++ b/lib/multistreamer/webapp.lua @@ -40,6 +40,8 @@ local ngx_log = ngx.log local ngx_warn = ngx.WARN local ngx_debug = ngx.DEBUG +local importexport = require'multistreamer.importexport' + local pid = ngx.worker.pid() app.views_prefix = 'multistreamer.views' @@ -133,6 +135,28 @@ app:match('logout', config.http_prefix .. '/logout', function(self) return { redirect_to = self:url_for('site-root') } end) +app:match('stream-export', config.http_prefix .. '/stream/:id/export', respond_to({ + before = function(self) + if not require_login(self) then return err_out(self,self.config.lang.login_required) end + + self.stream = Stream:find({ id = self.params.id }) + if not self.stream then + return err_out(self,self.config.lang.stream_not_found) + end + end, + GET = function(self) + local t = importexport.export_stream(self,self.stream) + return self:write({ + layout = 'plain', + content_type = 'application/octet-stream', + headers = { + ['Content-Disposition'] = 'attachment; filename="'.. self.stream.name .. '.json"', + }, + status = 200, + }, to_json(t.json)) + end, +})) + app:match('stream-edit', config.http_prefix .. '/stream(/:id)', respond_to({ before = function(self) if not require_login(self) then return err_out(self,self.config.lang.login_required) end @@ -316,6 +340,24 @@ app:match('stream-edit', config.http_prefix .. '/stream(/:id)', respond_to({ if not self.stream:check_owner(self.user) then return err_out(self,self.config.lang.stream_not_found) end + + if self.params['export_button'] ~= nil then + return self:write({ redirect_to = self:url_for('stream-export', {id = self.stream.id}) }) + end + + if self.params['import_file'] ~= nil and self.params['import_file'].content ~= nil and len(self.params['import_file'].content) > 0 then + local js = from_json(self.params['import_file'].content) + if js ~= nil then + js.stream.name = nil + local r = importexport.import_stream(self,self.stream,js.stream) + if r.status == 200 then + self.session.status_msg = { type = 'success', msg = self.config.lang.stream_import_success } + else + self.session.status_msg = { type = 'error', msg = self.config.lang.stream_import_error } + end + end + end + if not self.params.ffmpeg_pull_args or len(self.params.ffmpeg_pull_args) == 0 then if self.stream.ffmpeg_pull_args ~= nil then self.stream:update({ffmpeg_pull_args = db.NULL}) diff --git a/rockspecs/multistreamer-12.2.0-0.rockspec b/rockspecs/multistreamer-12.2.0-0.rockspec new file mode 100644 index 0000000..df4c230 --- /dev/null +++ b/rockspecs/multistreamer-12.2.0-0.rockspec @@ -0,0 +1,96 @@ +package = "multistreamer" +version = "12.2.0-0" + +source = { + url = "https://github.com/jprjr/multistreamer/archive/12.2.0.tar.gz", + file = "multistreamer-12.2.0.tar.gz", +} + +dependencies = { + "lua >= 5.1", + "lua-resty-exec", + "lua-resty-jit-uuid", + "lua-resty-http", + "lapis", + "etlua", + "luacrypto", + "luaposix", + "luafilesystem", + "lyaml", + "redis-lua", + "md5", +} + +build = { + type = "none", + install = { + bin = { + ["multistreamer"] = "bin/multistreamer.lua", + }, + conf = { + ["config.yaml"] = "etc/config.yaml.example", + }, + lua = { + ["multistreamer.api.v1"] = "lib/multistreamer/api/v1.lua", + ["multistreamer.chat_manager"] = "lib/multistreamer/chat_manager.lua", + ["multistreamer.cli"] = "lib/multistreamer/cli.lua", + ["multistreamer.config"] = "lib/multistreamer/config.lua", + ["multistreamer.getopt"] = "lib/multistreamer/getopt.lua", + ["multistreamer.http"] = "lib/multistreamer/http.lua", + ["multistreamer.importexport"] = "lib/multistreamer/importexport.lua", + ["multistreamer.migrations"] = "lib/multistreamer/migrations.lua", + ["multistreamer.nginx-conf"] = "lib/multistreamer/nginx-conf.lua", + ["multistreamer.process_manager"] = "lib/multistreamer/process_manager.lua", + ["multistreamer.redis"] = "lib/multistreamer/redis.lua", + ["multistreamer.shell"] = "lib/multistreamer/shell.lua", + ["multistreamer.string"] = "lib/multistreamer/string.lua", + ["multistreamer.version"] = "lib/multistreamer/version.lua", + ["multistreamer.webapp"] = "lib/multistreamer/webapp.lua", + ["multistreamer.irc"] = "lib/multistreamer/irc.lua", + ["multistreamer.irc.client"] = "lib/multistreamer/irc/client.lua", + ["multistreamer.irc.server"] = "lib/multistreamer/irc/server.lua", + ["multistreamer.irc.state"] = "lib/multistreamer/irc/state.lua", + ["multistreamer.models"] = "lib/multistreamer/models.lua", + ["multistreamer.models.account"] = "lib/multistreamer/models/account.lua", + ["multistreamer.models.keystore"] = "lib/multistreamer/models/keystore.lua", + ["multistreamer.models.shared_account"] = "lib/multistreamer/models/shared_account.lua", + ["multistreamer.models.shared_stream"] = "lib/multistreamer/models/shared_stream.lua", + ["multistreamer.models.stream"] = "lib/multistreamer/models/stream.lua", + ["multistreamer.models.stream_account"] = "lib/multistreamer/models/stream_account.lua", + ["multistreamer.models.user"] = "lib/multistreamer/models/user.lua", + ["multistreamer.models.webhook"] = "lib/multistreamer/models/webhook.lua", + ["multistreamer.networks"] = "lib/multistreamer/networks.lua", + ["multistreamer.networks.facebook"] = "lib/multistreamer/networks/facebook.lua", + ["multistreamer.networks.mixer"] = "lib/multistreamer/networks/mixer.lua", + ["multistreamer.networks.rtmp"] = "lib/multistreamer/networks/rtmp.lua", + ["multistreamer.networks.twitch"] = "lib/multistreamer/networks/twitch.lua", + ["multistreamer.networks.youtube"] = "lib/multistreamer/networks/youtube.lua", + ["multistreamer.views.account"] = "lib/multistreamer/views/account.etlua", + ["multistreamer.views.account-delete"] = "lib/multistreamer/views/account-delete.etlua", + ["multistreamer.views.account-share"] = "lib/multistreamer/views/account-share.etlua", + ["multistreamer.views.chat"] = "lib/multistreamer/views/chat.etlua", + ["multistreamer.views.chat-widget-config"] = "lib/multistreamer/views/chat-widget-config.etlua", + ["multistreamer.views.chatlayout"] = "lib/multistreamer/views/chatlayout.etlua", + ["multistreamer.views.index"] = "lib/multistreamer/views/index.etlua", + ["multistreamer.views.layout"] = "lib/multistreamer/views/layout.etlua", + ["multistreamer.views.login"] = "lib/multistreamer/views/login.etlua", + ["multistreamer.views.plain"] = "lib/multistreamer/views/plain.etlua", + ["multistreamer.views.profile"] = "lib/multistreamer/views/profile.etlua", + ["multistreamer.views.simplelayout"] = "lib/multistreamer/views/simplelayout.etlua", + ["multistreamer.views.stream"] = "lib/multistreamer/views/stream.etlua", + ["multistreamer.views.stream-accounts"] = "lib/multistreamer/views/stream-accounts.etlua", + ["multistreamer.views.stream-advanced"] = "lib/multistreamer/views/stream-advanced.etlua", + ["multistreamer.views.stream-dashboard"] = "lib/multistreamer/views/stream-dashboard.etlua", + ["multistreamer.views.stream-delete"] = "lib/multistreamer/views/stream-delete.etlua", + ["multistreamer.views.stream-functions"] = "lib/multistreamer/views/stream-functions.etlua", + ["multistreamer.views.stream-menu"] = "lib/multistreamer/views/stream-menu.etlua", + ["multistreamer.views.stream-permissions"] = "lib/multistreamer/views/stream-permissions.etlua", + ["multistreamer.views.stream-webhooks"] = "lib/multistreamer/views/stream-webhooks.etlua", + ["multistreamer.websocket.server"] = "lib/multistreamer/websocket/server.lua", + }, + }, + copy_directories = { + "share/multistreamer/html", + }, +} + diff --git a/rockspecs/multistreamer-template.rockspec b/rockspecs/multistreamer-template.rockspec index 27b4d85..5cec78b 100644 --- a/rockspecs/multistreamer-template.rockspec +++ b/rockspecs/multistreamer-template.rockspec @@ -37,6 +37,7 @@ build = { ["multistreamer.config"] = "lib/multistreamer/config.lua", ["multistreamer.getopt"] = "lib/multistreamer/getopt.lua", ["multistreamer.http"] = "lib/multistreamer/http.lua", + ["multistreamer.importexport"] = "lib/multistreamer/importexport.lua", ["multistreamer.migrations"] = "lib/multistreamer/migrations.lua", ["multistreamer.nginx-conf"] = "lib/multistreamer/nginx-conf.lua", ["multistreamer.process_manager"] = "lib/multistreamer/process_manager.lua",