diff --git a/applications/luci-app-51ddns/Makefile b/applications/luci-app-51ddns/Makefile new file mode 100644 index 000000000000..18cd669567df --- /dev/null +++ b/applications/luci-app-51ddns/Makefile @@ -0,0 +1,14 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for 51DDNS remote access +LUCI_URL:=https://github.com/21hkcloud/51ddns-openwrt +LUCI_DESCRIPTION:=Configure and monitor the 51DDNS remote access agent. +LUCI_DEPENDS:=+luci-base +51ddns-agent +LUCI_PKGARCH:=all + +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=Jinshuan Wang <21hkcloud@gmail.com> + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js b/applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js new file mode 100644 index 000000000000..9d78119ae3f0 --- /dev/null +++ b/applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js @@ -0,0 +1,141 @@ +'use strict'; +'require view'; +'require form'; +'require rpc'; +'require uci'; + +const callServiceList = rpc.declare({ + object: 'service', + method: 'list', + params: [ 'name' ], + expect: { '': {} }, +}); + +const callLocalStatus = rpc.declare({ + object: 'luci.51ddns', + method: 'status', + expect: { '': {} }, +}); + +const callAgentInfo = rpc.declare({ + object: 'luci.51ddns', + method: 'info', + expect: { '': {} }, +}); + +function serviceState(data) { + const instances = data?.['51ddns-agent']?.instances || {}; + const running = Object.values(instances).some(instance => instance?.running); + + return { + running, + label: running ? _('Running') : _('Not running'), + className: running ? 'alert-message success' : 'alert-message warning', + }; +} + +function formatDate(value) { + const date = new Date(value || ''); + + if (Number.isNaN(date.getTime())) + return _('Synchronizing'); + + return date.toLocaleString([], { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); +} + +function remainingState(value) { + const expires = new Date(value || ''); + + if (Number.isNaN(expires.getTime())) + return { label: _('Synchronizing'), className: 'alert-message notice' }; + + const milliseconds = expires.getTime() - Date.now(); + if (milliseconds <= 0) + return { label: _('Expired'), className: 'alert-message error' }; + + const hours = Math.ceil(milliseconds / 3600000); + const days = Math.ceil(hours / 24); + const label = hours > 48 + ? N_(days, 'About %d day', 'About %d days').format(days) + : N_(hours, '%d hour', '%d hours').format(hours); + + return { + label, + className: milliseconds <= 3 * 86400000 + ? 'alert-message warning' + : 'alert-message success', + }; +} + +return view.extend({ + load() { + return Promise.all([ + uci.load('51ddns'), + L.resolveDefault(callServiceList('51ddns-agent'), {}), + L.resolveDefault(callLocalStatus(), {}), + L.resolveDefault(callAgentInfo(), {}), + ]); + }, + + render(data) { + const state = serviceState(data[1]); + const local = data[2] || {}; + const info = data[3] || {}; + const plan = local.plan || null; + const remaining = remainingState(plan?.expires_at); + const map = new form.Map( + '51ddns', + _('51DDNS Remote Access'), + _('Enter the account token once to register this router automatically. Device identity and relay settings are managed by the 51DDNS control plane.'), + ); + const section = map.section(form.NamedSection, 'main', 'agent', _('Quick setup')); + section.addremove = false; + + const status = section.option(form.DummyValue, '_status', _('Service status')); + status.rawhtml = true; + status.cfgvalue = () => + `${state.label}`; + + const version = section.option(form.DummyValue, '_version', _('Agent version')); + version.cfgvalue = () => info.version || _('Unavailable'); + + const planName = section.option(form.DummyValue, '_plan_name', _('Current plan')); + planName.cfgvalue = () => plan?.product_name || _('No active plan'); + + const expiresAt = section.option(form.DummyValue, '_expires_at', _('Expires at')); + expiresAt.cfgvalue = () => plan ? formatDate(plan.expires_at) : _('Not bound'); + + const remainingTime = section.option(form.DummyValue, '_remaining_time', _('Time remaining')); + remainingTime.rawhtml = true; + remainingTime.cfgvalue = () => { + if (!plan) + return `${_('Bind or purchase a plan')}`; + + return `${remaining.label}`; + }; + + const consoleButton = section.option(form.Button, '_console', _('Console')); + consoleButton.inputtitle = _('Open 51DDNS Console'); + consoleButton.inputstyle = 'apply'; + consoleButton.onclick = () => + window.open('https://console.51ddns.com/console#/workbench', '_blank', 'noopener,noreferrer'); + + const enabled = section.option(form.Flag, 'enabled', _('Enable')); + enabled.rmempty = false; + enabled.default = enabled.disabled; + + const token = section.option(form.Value, 'account_token', _('Account token')); + token.password = true; + token.rmempty = false; + token.placeholder = '51d_...'; + token.description = _('Copy the token from the 51DDNS console. All devices in the same account share this token.'); + + return map.render(); + }, +}); diff --git a/applications/luci-app-51ddns/po/templates/51ddns.pot b/applications/luci-app-51ddns/po/templates/51ddns.pot new file mode 100644 index 000000000000..c4879cd78492 --- /dev/null +++ b/applications/luci-app-51ddns/po/templates/51ddns.pot @@ -0,0 +1,108 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:66 +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "" +msgstr[1] "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:94 +#: applications/luci-app-51ddns/root/usr/share/luci/menu.d/luci-app-51ddns.json:3 +msgid "51DDNS Remote Access" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:65 +msgid "About %d day" +msgid_plural "About %d days" +msgstr[0] "" +msgstr[1] "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:133 +msgid "Account token" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:105 +msgid "Agent version" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:118 +msgid "Bind or purchase a plan" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:123 +msgid "Console" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:137 +msgid "" +"Copy the token from the 51DDNS console. All devices in the same account " +"share this token." +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:108 +msgid "Current plan" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:129 +msgid "Enable" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:95 +msgid "" +"Enter the account token once to register this router automatically. Device " +"identity and relay settings are managed by the 51DDNS control plane." +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:60 +msgid "Expired" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:111 +msgid "Expires at" +msgstr "" + +#: applications/luci-app-51ddns/root/usr/share/rpcd/acl.d/luci-app-51ddns.json:3 +msgid "Grant access to the 51DDNS configuration" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:109 +msgid "No active plan" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:112 +msgid "Not bound" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:32 +msgid "Not running" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:124 +msgid "Open 51DDNS Console" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:97 +msgid "Quick setup" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:32 +msgid "Running" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:100 +msgid "Service status" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:41 +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:56 +msgid "Synchronizing" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:114 +msgid "Time remaining" +msgstr "" + +#: applications/luci-app-51ddns/htdocs/luci-static/resources/view/51ddns/overview.js:106 +msgid "Unavailable" +msgstr "" diff --git a/applications/luci-app-51ddns/root/usr/libexec/rpcd/luci.51ddns b/applications/luci-app-51ddns/root/usr/libexec/rpcd/luci.51ddns new file mode 100755 index 000000000000..da0b935d4afc --- /dev/null +++ b/applications/luci-app-51ddns/root/usr/libexec/rpcd/luci.51ddns @@ -0,0 +1,59 @@ +#!/bin/sh + +. /usr/share/libubox/jshn.sh + +print_status() { + status_has_plan=0 + status_product_name='' + status_expires_at='' + status_payload='' + + if [ -s /var/lib/51ddns/status.json ]; then + status_payload="$(cat /var/lib/51ddns/status.json 2>/dev/null)" + json_load "$status_payload" 2>/dev/null + + if json_select plan >/dev/null 2>&1; then + status_has_plan=1 + json_get_var status_product_name product_name + json_get_var status_expires_at expires_at + fi + json_cleanup + fi + + json_init + if [ "$status_has_plan" = 1 ]; then + json_add_object plan + json_add_string product_name "$status_product_name" + json_add_string expires_at "$status_expires_at" + json_close_object + fi + json_dump + json_cleanup +} + +case "$1" in + list) + echo '{"status":{},"info":{}}' + ;; + call) + case "$2" in + status) + print_status + ;; + info) + version="$(/usr/bin/51ddns-agent --version 2>/dev/null | head -n 1)" + json_init + json_add_string version "$version" + json_dump + json_cleanup + ;; + *) + echo '{"error":"method not found"}' + exit 1 + ;; + esac + ;; + *) + exit 1 + ;; +esac diff --git a/applications/luci-app-51ddns/root/usr/share/luci/menu.d/luci-app-51ddns.json b/applications/luci-app-51ddns/root/usr/share/luci/menu.d/luci-app-51ddns.json new file mode 100644 index 000000000000..1f7b5bedba5c --- /dev/null +++ b/applications/luci-app-51ddns/root/usr/share/luci/menu.d/luci-app-51ddns.json @@ -0,0 +1,13 @@ +{ + "admin/services/51ddns": { + "title": "51DDNS Remote Access", + "order": 35, + "action": { + "type": "view", + "path": "51ddns/overview" + }, + "depends": { + "acl": [ "luci-app-51ddns" ] + } + } +} diff --git a/applications/luci-app-51ddns/root/usr/share/rpcd/acl.d/luci-app-51ddns.json b/applications/luci-app-51ddns/root/usr/share/rpcd/acl.d/luci-app-51ddns.json new file mode 100644 index 000000000000..0f8581b3c44a --- /dev/null +++ b/applications/luci-app-51ddns/root/usr/share/rpcd/acl.d/luci-app-51ddns.json @@ -0,0 +1,15 @@ +{ + "luci-app-51ddns": { + "description": "Grant access to the 51DDNS configuration", + "read": { + "ubus": { + "luci.51ddns": [ "status", "info" ], + "service": [ "list" ] + }, + "uci": [ "51ddns" ] + }, + "write": { + "uci": [ "51ddns" ] + } + } +}