various bugfixes + clouddrive update

This commit is contained in:
Leon van Kammen 2026-08-03 19:19:07 +02:00
parent 50b0fbbba4
commit 7cd6df24c7
17 changed files with 366 additions and 117 deletions

4
.env
View file

@ -1,6 +1,6 @@
# aliases # aliases
build(){ $(pwd)/util/xrforge.sh build; } build(){ $(pwd)/src/util/xrforge.sh build; }
rebuild(){ $(pwd)/util/xrforge.sh rebuild; } rebuild(){ $(pwd)/src/util/xrforge.sh rebuild; }
# forgejo server hook # forgejo server hook
#test "$GITEA_ROOT_URL" = "https://xrforge.isvery.ninja/" && { #test "$GITEA_ROOT_URL" = "https://xrforge.isvery.ninja/" && {

View file

@ -1,4 +1,65 @@
XRForge is a tiny, powerful tool to interconnect XR experiences.<br> XRForge is a tiny, powerful tool to interconnect XR experiences.<br>
<br>
An open, seamless XR web built from simple URLs and directories, without gatekeepers.<br>
<br>
<br>
XRForge transforms standard local or remote file directories into interconnected 3D spaces.
<br>Users can seamlessly "surf" the spatial web, turning everyday directories into 3D portals.
```
┌───────────────────────────────────┐ ┌──────────────────────────────────────────┐
│ local disk │ │ Surf via 3D portals │
├───────────────────────────────────┤ └──────────────────────────────────────────┘
│ │
│ /myverse │ https://my.org/myverse
│ /elearnings │ https://my.org/myverse/elearnings
│ /elearning1/1.glb │ https://my.org/myverse/elearning/elearning1
│ /elearning2/2.glb │ https://my.org/myverse/elearning/elearning2
│ │
│ ┌──────────────────────────────┐ │
│ │ github / webdav / dropbox etc│ │
│ │ │ │
│ │ /mygames │ │ https://my.org/myverse/mygames
│ │ /biggusgateWebXR │ │ https://my.org/myverse/mygames/biggusgate
│ │ /spasca │ │ https://my.org/myverse/mygames/spasca
│ │ /otherxrforge │ │ https://my.org/myverse/otherxrforge
│ │ │ │
│ │ ┌───────────┐ ┌───────────┐ │ │
│ │ │/manyfold │ │archive.org│ │ │ https://my.org/myverse/manyfold
│ │ └───────────┘ └───────────┘ │ │ https://my.org/myverse/myarchive
│ │ │ │
│ │ +40 integrations! │ │
│ └──────────────────────────────┘ │
└───────────────────────────────────┘
```
## Usage / Radical Opensource
```bash
$ ./xrforge.com
Usage: ./xrforge.com <cmd> [opts]
./xrforge.com test run testsuite (selftest)
./xrforge.com admin launch adminpanel to configure stuff (/admin)
./xrforge.com generate generate www-folder with XR experiences
./xrforge.com help display all possible flags
./xrforge.com createplugin generates skeleton plugin
./xrforge.com server starts the webserver [without --nobrowser]
./xrforge.com build build
$ unzip xrforge.com
Archive: result/bin/xrforge.com
Length Date Time Name
--------- ---------- ----- ----
3992 08-03-2026 09:54 .init.lua
242 08-03-2026 09:46 cmd/server.lua
1014 07-23-2026 16:13 cmd/createplugin.lua
580 07-24-2026 16:12 cmd/help.lua
....
```
> Profit! you have aqcuired the sourcecode from the binary: run and distribute modified versions if needed ♥
## Adding content ## Adding content

View file

@ -43,6 +43,8 @@ app = require("soakbean") {
end end
} }
util.fetchInstall(app) -- setup internal fetch callback mechanism
-- api skeleton (implemented via soakbean's app.on(...)) -- api skeleton (implemented via soakbean's app.on(...))
app.set = function(k,v) end app.set = function(k,v) end
app.init = function() end app.init = function() end
@ -55,10 +57,10 @@ app.plugin.diskscan = app.addPlugin('plugin/diskscan')
app.plugin.moderation = app.addPlugin('plugin/moderation') app.plugin.moderation = app.addPlugin('plugin/moderation')
app.plugin.xrfragments = app.addPlugin('plugin/xrfragments') app.plugin.xrfragments = app.addPlugin('plugin/xrfragments')
app.plugin.janusxr = app.addPlugin('plugin/janusxr') app.plugin.janusxr = app.addPlugin('plugin/janusxr')
app.plugin.xrforges = app.addPlugin('plugin/xrforges') app.plugin.clouddrive = app.addPlugin('plugin/clouddrive')
app.plugin.federation = app.addPlugin('plugin/federation')
app.plugin.manyfold = app.addPlugin('plugin/manyfold') app.plugin.manyfold = app.addPlugin('plugin/manyfold')
app.plugin.git = app.addPlugin('plugin/git') app.plugin.git = app.addPlugin('plugin/git')
app.plugin.clouddrive = app.addPlugin('plugin/clouddrive')
app.plugin.webhook = app.addPlugin('plugin/webhook') app.plugin.webhook = app.addPlugin('plugin/webhook')
app.plugin.markdown = app.addPlugin('plugin/markdown') app.plugin.markdown = app.addPlugin('plugin/markdown')
@ -104,6 +106,11 @@ end)
function OnHttpRequest() app.run() end function OnHttpRequest() app.run() end
OnServerListen = function(socketdescriptor, serverip, serverport)
app.ip = FormatIp(serverip)
app.port = serverport
end
OnServerStart = function() OnServerStart = function()
app.init() -- this triggers init event app.init() -- this triggers init event
end end

View file

@ -188,7 +188,7 @@ sb = {
header=GetHeaders(), header=GetHeaders(),
url=GetPath(), url=GetPath(),
protocol=GetScheme(), protocol=GetScheme(),
body={} body=GetBody(),
} }
local res={ local res={
_status=nil, _status=nil,

View file

@ -75,10 +75,17 @@ util.templateFile = function(file,data)
return util.template( LoadAsset(file), data) return util.template( LoadAsset(file), data)
end end
util.getURL = function() util.getURL = function(url,opts)
local port = ':' .. GetPort() opts = opts or {}
local port = ''
if opts.server then
url = url or '/'
return 'http://localhost:' .. app.port .. url
else
port = ':' .. GetPort()
if port == 80 then port = '' end if port == 80 then port = '' end
return GetScheme() .. '://' .. GetHost() .. port .. GetPath() return GetScheme() .. '://' .. GetHost() .. port .. (url or GetPath())
end
end end
util.template = function(content,data) util.template = function(content,data)
@ -287,6 +294,30 @@ function util.pluck(path, tbl, default)
return current return current
end end
function util.fetchInstall(app)
app.expectedreq = {}
app.post('/bus',function(req,res,next)
local reqid = GetParam('id')
local cb = app.expectedreq[reqid]
if cb then cb(req,res,next) end
end)
end
function util.fetch(url, opts, cb)
local reqid = tostring( Rdrand() )
app.expectedreq[reqid] = cb
local urlret = util.getURL('/bus?id=' .. reqid, {server=true} )
if assert(unix.fork()) == 0 then
local ok, headers, body = Fetch(url, opts)
if ok then
Fetch(urlret, body) -- post back to ourselves
else
print(headers)
end
unix.exit(0)
end
end
function util.stripPath(str) function util.stripPath(str)
return str:gsub("^./",""):gsub("^/",""):gsub("/$","") return str:gsub("^./",""):gsub("^/",""):gsub("/$","")

View file

@ -1,4 +1,6 @@
return function(app,argv) return function(app,argv)
app.plugin.diskscan.scan() print('generating..')
app.plugin.clouddrive.mount()
--app.plugin.diskscan.scan( app.opts.plugin_diskscan_dir )
os.exit() os.exit()
end end

View file

@ -270,7 +270,11 @@ img.icon{ display:none; }
.info{ .info{
background:#224; background:#224;
padding:10px; padding:10px 20px;
overflow:clip;
}
.info .tag{
margin-bottom:7px;
} }
b.tag{ b.tag{

4
src/federation.json Normal file
View file

@ -0,0 +1,4 @@
[
{"url":"https://xrforge.isvery.ninja", "name":"XRForge", "thumbnail":""},
{"url":"https://www.janusxr.org", "name":"JanusXR", "thumbnail":""}
]

View file

@ -1,6 +1,6 @@
local util = require("util") local util = require("util")
local data = util.merge({notification=''},{ opts = app.opts }) local data = {notification=''}
data.plugins = "" data.plugins = ""
data.tabs = {} data.tabs = {}
@ -15,13 +15,13 @@ function pluginInput(opt)
</td> </td>
<td> <td>
]] ]]
local value = app.opts[ opt.key:gsub("^app.opts.","") ] local key = opt.key:gsub("^app.opts.","")
local value = app.opts[ key ]
if type(opt.values) == 'table' then if type(opt.values) == 'table' then
html = html .. util.template('<select name="${key}" value="${value}">\n', { html = html .. util.template('<select name="${key}" value="${value}">\n', {
key = opt.key, key = opt.key,
value = value value = value
}) })
print(opt.key:gsub("^app.opts.","") )
foreach( opt.values, function(k,v) foreach( opt.values, function(k,v)
html = html .. util.template('<option value="${k}">${v}</option>\n',{ html = html .. util.template('<option value="${k}">${v}</option>\n',{
k=k, k=k,
@ -30,21 +30,21 @@ function pluginInput(opt)
end) end)
html = html .. "</select>\n" html = html .. "</select>\n"
else else
html = html .. util.template('<input type="text" name="${key}" value="${value}"></input>',{ html = html .. util.template('<input type="text" name="${key}" value="${value}" placeholder="${placeholder}"></input>',{
key = opt.key, key = opt.key,
value = value value = value,
placeholder= opt.placeholder or ''
}) })
end end
html = html .. util.template([[ html = html .. util.template([[
</td> </td>
<td class="col3">${info}</td> <td class="col3">${info}</td>
</td>
</tr> </tr>
]],opt) ]],opt)
return html return html
end end
function pluginUI(seen) function pluginUI(seen,data)
return function(name,plugin) return function(name,plugin)
if seen[ plugin.name ] then return end -- already processed if seen[ plugin.name ] then return end -- already processed
table.insert( data.tabs, plugin.name ) -- insert tabname table.insert( data.tabs, plugin.name ) -- insert tabname
@ -63,8 +63,8 @@ function saveData()
local params = GetParams() local params = GetParams()
local saveconfig = false local saveconfig = false
foreach(params, function(k,v) foreach(params, function(k,v)
if v[1]:match("^opts.") then if v[1]:match("^app%.opts%.") then
app.opts[ v[1]:gsub("^opts.","") ] = v[2] -- update app.opts[ v[1]:gsub("^app%.opts%.","") ] = v[2] -- update
saveconfig = true saveconfig = true
end end
end) end)
@ -77,9 +77,11 @@ function saveData()
end end
end end
local seen = {}
foreach( app.plugin, pluginUI(seen) )
saveData() saveData()
data = util.merge(data,{ opts = app.opts })
local seen = {}
foreach( app.plugin, pluginUI(seen,data) )
local html = util.template( LoadAsset("page/admin/index.html"),data) local html = util.template( LoadAsset("page/admin/index.html"),data)

View file

@ -15,11 +15,21 @@
<td></td> <td></td>
<td class=""> <td class="">
{if #plugin.info > 2 then {if #plugin.info > 2 then
if plugin.infowide then
print([[
</td>
<tr><td colspan="3">
<div class="col3 info">
${plugin.info}
</div>
]])
else
print([[ print([[
<div class="col3 info"> <div class="col3 info">
${plugin.info} ${plugin.info}
</div> </div>
]]) ]])
end
end} end}
<br/><br/> <br/><br/>
</td> </td>

View file

@ -21,6 +21,7 @@ return {
local matchpath = urlpath local matchpath = urlpath
if urlpath == '/' then matchpath = rootpath end -- generate janusxr footer for root url if urlpath == '/' then matchpath = rootpath end -- generate janusxr footer for root url
foreach( app.cache.items, function(k,v) foreach( app.cache.items, function(k,v)
if req.authenticated or app.plugin.moderation.isAllowed(v) then
if v.parent then if v.parent then
trace( "path = " .. util.stripPath(v.path) .. " " .. trace( "path = " .. util.stripPath(v.path) .. " " ..
"parent = " .. util.stripPath(v.parent.path) .. " " .. "parent = " .. util.stripPath(v.parent.path) .. " " ..
@ -43,7 +44,7 @@ return {
res.items[ v.id ] = v res.items[ v.id ] = v
res.parent = parent res.parent = parent
end end
end
end end
end) end)
local data = util.merge({ local data = util.merge({
@ -72,6 +73,7 @@ return {
if data.item then if data.item then
res.template = 'page/index.html' res.template = 'page/index.html'
end end
app.pub("dir", nil, {res=res,data=data, urlpath=urlpath, isdir=isdir})
app.cache.url[ urlpath ] = util.templateFile( res.template, data) app.cache.url[ urlpath ] = util.templateFile( res.template, data)
end end
if redirect then if redirect then

View file

@ -7,9 +7,9 @@
end end
end} end}
</assets> </assets>
<room pos="0 0 -4" skybox="true" showavatar="false" use_local_asset="room_plane"> <room pos="0 0 4" skybox="true" showavatar="false" use_local_asset="room_plane" zdir="0 0 -1">
<light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" /> <light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" />
<text js_id="location" pos="0 6 -5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${url}" rotation="-180 0 -180" emissive="0 0 0" roughness="0.3" metalness="0" /> <text js_id="location" pos="0 6 -5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${url}" emissive="0 0 0" roughness="0.3" metalness="0" />
{if item then {if item then
print('\t<object id="${url}"/>') print('\t<object id="${url}"/>')
end} end}
@ -22,7 +22,7 @@
local center = -((#items*width)/1.3) + (i*width) local center = -((#items*width)/1.3) + (i*width)
local url = v.url local url = v.url
if v.dir then url = url .. "/" end if v.dir then url = url .. "/" end
print('\t\t<object js_id="portal_' .. i .. '" pos="' .. center .. ' 0 0" rotation="0 180 0">') print('\t\t<object js_id="portal_' .. i .. '" pos="' .. center .. ' 0 0">')
print('\t\t <link url="' .. url .. '" thumb_id="img' .. i .. '" title_col="0.4 0.37 0.4" pos="0 0 0.001" draw_glow="true" scale="3 3 0.1" />') print('\t\t <link url="' .. url .. '" thumb_id="img' .. i .. '" title_col="0.4 0.37 0.4" pos="0 0 0.001" draw_glow="true" scale="3 3 0.1" />')
print('\t\t <link url="' .. url .. '" shader_id="xrforgeportal" pos="0 0 0.003" draw_glow="false" scale="3 3 0.1" />') print('\t\t <link url="' .. url .. '" shader_id="xrforgeportal" pos="0 0 0.003" draw_glow="false" scale="3 3 0.1" />')
print('\t\t <text text="' .. v.title .. '" pos="0 3.4 0" col="1 1 1" font_scale="false" scale="0.2 0.2 0.2" />') print('\t\t <text text="' .. v.title .. '" pos="0 3.4 0" col="1 1 1" font_scale="false" scale="0.2 0.2 0.2" />')

View file

@ -1,5 +1,5 @@
local util = require('util') local util = require('util')
local remotes = {"-- disabled --"} local dir = require('opendirectory')
app.on('scan_experience', function(exp) app.on('scan_experience', function(exp)
--print_r(exp) --print_r(exp)
@ -12,65 +12,113 @@ app.on('set', function(k,v)
print("content was updated") print("content was updated")
end) end)
if util.cmdExist('rclone') then -- this registers the plugin UI
local stdout = util.execute('rclone listremotes 2>&1') local plugin = {
print('stdout = ' .. stdout) name = 'Cloud drives',
info = '',
infowide = true,
opts = { },
mount = function()
if util.cmdExist('rclone') then
local me = app.plugin.clouddrive
for i=1,5 do
local k = 'plugin_clouddrive_remote_' .. i
local mapping = app.opts[k]
if mapping and #mapping > 0 and not mapping:match("%.%.") then
local dir = '.' .. mapping:gsub(".* ","")
local remote = mapping:gsub(" .*","")
os.execute('test -d ' .. dir .. ' || mkdir ' .. dir)
local cmd = 'rclone mount --no-checksum --no-modtime --no-seek --allow-other --daemon ' .. remote .. ' ' .. dir
stdout, retstr, retcode = os.execute('mountpoint ' .. dir )
if retcode ~= 0 then
print("please run: " .. cmd)
end
end
end
else
print('[!] rclone not detected..will skip mounting remotes')
end
end
}
function scanRemotes(remotes)
local stdout, retstr, retcode = util.execute('rclone listremotes 2>&1')
local lines = util.split(stdout,"\n") local lines = util.split(stdout,"\n")
foreach( lines, function(k,v) foreach( lines, function(k,v)
local remote = v:gsub(":$","") local remote = v:gsub(":$","")
remotes[ remote ] = remote remotes[ remote ] = remote
end) end)
if util.count(remotes) > 0 then
plugin.info = plugin.info .. '<br><br><a href="https://rclone.org" target="_blank">rclone</a> remotes detected:<br><br>'
foreach(remotes, function(k,remote)
plugin.info = plugin.info .. '<b class="tag">' .. remote .. ':/</b>&nbsp;'
end)
end
end end
function scanMountables(mountables)
dir.foreach( '/mnt', function(path,stat,state,name,kind,ino,off,attr,path)
if kind == unix.DT_DIR then
table.insert( mountables, name )
end
end)
if #mountables > 0 then
plugin.info = plugin.info .. '<br><br>mountables found:<br><br>'
foreach(mountables, function(k,mount)
if mount ~= '..' then
plugin.info = plugin.info .. '<b class="tag">/mnt/' .. mount .. '</b>&nbsp;'
end
end)
end
end
-- this registers the plugin UI function scanMounts(mounts)
local plugin = { dir.foreach( app.opts.plugin_diskscan_dir, function(path,stat,state,name,kind,ino,off,attr,fullpath)
name = 'Cloud drives <b class="tag">disabled</b>', local stdout = util.execute("mountpoint " .. fullpath)
info = '', if stdout:match("is a mountpoint") then
opts = { local mount = {to=fullpath}
{ mount.from = util.execute('readlink '.. fullpath)
key='app.opts.plugin_clouddrive_remote_1', table.insert( mounts, mount )
title='Cloud 1', end
info='<a href="https://rclone.org">rclone</a> remotes show up in the dropdown (if installed)', end)
default="-- disabled --", if #mounts > 0 then
values=remotes plugin.info = plugin.info .. '<br>active mounts found:<br><br>'
}, foreach(mounts, function(k,mount)
{ if mount ~= '..' then
key='app.opts.plugin_clouddrive_remote_1_target', plugin.info = plugin.info .. '<b class="tag">'.. mount.from .. ' </b> to URL <b class="tag">' .. mount.to .. '</b>&nbsp;'
title='Cloud 1 URL', end
info='at which URL should this folder show up?', end)
default=app.opts.plugin_diskscan_dir:sub(2) .. "/cloud1", end
values='' end
},
{ function scanBackends(backends)
key='app.opts.plugin_clouddrive_remote_2', local stdout, retstr, retcode = util.execute("rclone help backends 2>&1")
title='Cloud 2', local lines = util.split(stdout,"\n")
info='<a href="https://rclone.org">rclone</a> remotes show up in the dropdown (if installed)', plugin.info = plugin.info .. app.tpl([[
default="-- disabled --", <br><br>
values=remotes <pre>
}, ${stdout}
{ </pre>
key='app.opts.plugin_clouddrive_remote_2_target', ]], { stdout = stdout})
title='Cloud 2 URL', end
info='at which URL should this folder show up?',
default=app.opts.plugin_diskscan_dir:sub(2) .. "/cloud2", -- scan rclone remotes & mountpoints
values='' local mounts = {}
}, local mountables = {}
{ local remotes = {}
key='app.opts.plugin_clouddrive_remote_3', local backends = {}
title='Cloud 3', scanMounts(mounts)
info='<a href="https://rclone.org">rclone</a> remotes show up in the dropdown (if installed)', scanMountables(mountables)
default="-- disabled --",
values=remotes if util.cmdExist('rclone') then
}, scanRemotes(remotes)
{ scanBackends(backends)
key='app.opts.plugin_clouddrive_remote_3_target', else
title='Cloud 3 URL', plugin.info = plugin.info .. '<br><br>No (rclone) remotes available. Please make sure to install/configure remotes via <a href="https://rclone.org" target="_blank">rclone</a>, <a href="https://github.com/rclone-ui/rclone-ui" target="_blank">rclone-ui</a> or <a href="https://github.com/kapitainsky/RcloneBrowser" target="_blank">rclone-browser</a><br>'
info='at which URL should this folder show up?', end
default=app.opts.plugin_diskscan_dir:sub(2) .. "/cloud3",
values='' if not util.cmdExist('rs-mount') then
}, plugin.info = plugin.info .. '<br><a href="https://remotestorage.io" target="_blank">RemoteStorage</a> cannot be mounted, as <a href="https://github.com/remotestorage/fuse" target="_blank">rs-mount</a> is not installed (yet).<br><br>'
} end
}
return plugin return plugin

View file

@ -107,12 +107,14 @@ return {
-- check for directory thumbnail sidecarfile -- check for directory thumbnail sidecarfile
local thumbExt = {"png","jpg","webp"} local thumbExt = {"png","jpg","webp"}
foreach( thumbExt, function(k,ext) foreach( thumbExt, function(k,ext)
local thumbnailFile = fullpath .. '.' .. ext local files = { fullpath .. '.' .. ext, fullpath .. '/' .. name .. '.' .. ext }
foreach( files, function(kk,thumbnailFile)
if util.fileExist( thumbnailFile ) then if util.fileExist( thumbnailFile ) then
experience.thumbnail = "/" .. util.stripPath(thumbnailFile) experience.thumbnail = "/" .. util.stripPath(thumbnailFile)
print("✅ └─ plugin/diskscan: detected .".. ext:lower() .. " thumbnail file: " .. thumbnailFile) print("✅ └─ plugin/diskscan: detected .".. ext:lower() .. " thumbnail file: " .. thumbnailFile)
end end
end) end)
end)
-- scan children -- scan children
me.scanDir(fullpath,experience,seen,symlinks) me.scanDir(fullpath,experience,seen,symlinks)
end end

View file

@ -11,20 +11,80 @@ app.on('set', function(k,v)
print("content was updated") print("content was updated")
end) end)
--app.on('dir', function(opts)
-- local me = app.plugin.federation
-- if opts.urlpath == '/' then
-- foreach( me.portals, function(k,portal)
-- print("JAAA")
-- table.insert( opts.data.items,{
-- url = portal.url,
-- name = portal.name,
-- thumbnail = portal.thumbnail
-- })
-- end)
-- end
--end)
-- this registers the plugin UI
local plugin = { local plugin = {
name = 'Federation', name = 'Federation <b class="tag">disabled</b>',
info = '', info = 'updating values requires a xrforge restart',
opts = { -- TODO populate this based on xrforge-federation.json opts = { -- TODO populate this based on xrforge-federation.json
{ {
key='app.opts.plugin_xrforges_xrforge_isvery_ninja', key='app.opts.plugin_federation_url_1',
title='xrforge.isvery.ninja', title='Federation URL 1',
info='', info="a json with (janus)xr(forge) URLs",
default="enabled", default="./federation.json",
values={enabled='enabled',disabled='disabled'} values=''
},
{
key='app.opts.plugin_federation_url_2',
title='Federation URL 1',
info="a json with (janus)xr(forge) URLs",
default="",
values=''
},
{
key='app.opts.plugin_federation_url_3',
title='Federation URL 1',
info="a json with (janus)xr(forge) URLs",
default="",
values=''
} }
} }
} }
--
-- PROBLEM: every request has its own app context
-- so the util.fetch() didn't help here
-- sqlite is needed to share state
app.on('init', function()
local me = app.plugin.federation
me.portals = {}
local federations = {}
-- load previous federations if any
for k=1,3 do
if app.opts['plugin_federation_url_'..k] and #app.opts['plugin_federation_url_'..k] > 0 then
table.insert(federations, app.opts['plugin_federation_url_'..k])
end
end
foreach(federations, function(k,url)
if url:match('^[./]') then
url = util.getURL( url:gsub('^[./]',''), {server=true} )
end
util.fetch(url, nil, function(req,res,next)
if req.body then
local t = DecodeJson(req.body)
if t then
foreach( t, function(k,opt)
table.insert( me.portals, opts )
end)
end
end
end)
end)
end)
-- this registers the plugin UI
return plugin return plugin

View file

@ -23,7 +23,23 @@ local plugin = {
default = "wip beta -v[0-9] customerX", default = "wip beta -v[0-9] customerX",
values='' values=''
}, },
} },
isAllowed = function(exp)
local me = app.plugin.moderation
local allowed = true
if not me.keywords then
me.keywords = util.split(app.opts.plugin_moderation_mutekeywords," ")
end
foreach( me.keywords, function(k,pattern)
if exp.name:match(pattern)
or exp.title:match(pattern)
or exp.url:match(pattern)
then
allowed = false
end
end)
return allowed
end
} }
return plugin return plugin