simple support for nip07

This commit is contained in:
DASHU
2024-09-04 19:22:38 +08:00
parent 666c546d93
commit 633a29cd79
20 changed files with 1078 additions and 111 deletions

View File

@@ -1,15 +1,27 @@
import 'package:flutter/material.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/component/auth_dialog/auth_dialog_base_componnet.dart';
import 'package:nowser/const/connect_type.dart';
import 'package:nowser/const/reasonable_permissions.dart';
import 'package:nowser/data/app_db.dart';
import 'package:nowser/main.dart';
import 'package:nowser/util/router_util.dart';
import '../../const/base.dart';
import '../../data/app.dart';
class AuthAppConnectDialog extends StatefulWidget {
static void show(BuildContext context) {
showDialog(
App app;
AuthAppConnectDialog({required this.app});
static Future<App?> show(BuildContext context, App app) {
return showDialog(
context: context,
builder: (context) {
return AuthAppConnectDialog();
return AuthAppConnectDialog(
app: app,
);
},
);
}
@@ -65,7 +77,12 @@ class _AuthAppConnectDialog extends State<AuthAppConnectDialog> {
children: list,
);
return AuthDialogBaseComponnet(title: "App Connect", child: child);
return AuthDialogBaseComponnet(
app: widget.app,
title: "App Connect",
onConfirm: onConfirm,
child: child,
);
}
void onConnectTypeChange(int? value) {
@@ -75,4 +92,22 @@ class _AuthAppConnectDialog extends State<AuthAppConnectDialog> {
});
}
}
onConfirm() async {
var app = widget.app;
app.connectType = connectType;
if (StringUtil.isBlank(app.pubkey) && keyProvider.pubkeys.isNotEmpty) {
app.pubkey = keyProvider.pubkeys.first;
}
if (connectType == ConnectType.REASONABLE) {
app.alwaysAllow = ReasonablePermissions.text;
}
app.createdAt = DateTime.now().millisecondsSinceEpoch ~/ 1000;
app.updatedAt = app.createdAt;
if (await AppDB.insert(app) > 0) {
await appProvider.reload();
RouterUtil.back(context);
}
}
}

View File

@@ -1,10 +1,16 @@
import 'package:flutter/material.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/const/app_type.dart';
import 'package:nowser/const/base.dart';
import 'package:nowser/data/app.dart';
import '../app/app_type_component.dart';
class AuthAppInfoComponent extends StatefulWidget {
App app;
AuthAppInfoComponent({required this.app});
@override
State<StatefulWidget> createState() {
return _AuthAppInfoComponent();
@@ -16,6 +22,26 @@ class _AuthAppInfoComponent extends State<AuthAppInfoComponent> {
Widget build(BuildContext context) {
var themeData = Theme.of(context);
String? name;
String? des;
if (StringUtil.isNotBlank(widget.app.name)) {
name = widget.app.name;
des = widget.app.code;
} else if (StringUtil.isBlank(widget.app.name)) {
name = widget.app.code;
}
List<Widget> rightList = [];
if (StringUtil.isNotBlank(name)) {
rightList.add(Text(
name!,
style: TextStyle(fontWeight: FontWeight.bold),
));
}
if (StringUtil.isNotBlank(des)) {
rightList.add(Text(des!));
}
return Stack(
alignment: Alignment.center,
children: [
@@ -24,12 +50,13 @@ class _AuthAppInfoComponent extends State<AuthAppInfoComponent> {
height: 64,
child: Card(
child: Container(
padding: EdgeInsets.all(Base.BASE_PADDING_HALF),
padding: const EdgeInsets.all(Base.BASE_PADDING_HALF),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
margin: EdgeInsets.only(right: Base.BASE_PADDING_HALF),
margin:
const EdgeInsets.only(right: Base.BASE_PADDING_HALF),
child: Icon(
Icons.image,
size: 46,
@@ -38,13 +65,7 @@ class _AuthAppInfoComponent extends State<AuthAppInfoComponent> {
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"APP NAME",
style: TextStyle(fontWeight: FontWeight.bold),
),
Text("This is App info des"),
],
children: rightList,
)
],
),

View File

@@ -1,14 +1,39 @@
import 'package:flutter/material.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/component/auth_dialog/auth_dialog_base_componnet.dart';
import 'package:nowser/const/auth_result.dart';
import 'package:nowser/data/app.dart';
import 'package:nowser/util/router_util.dart';
import '../../const/base.dart';
class AuthDialog extends StatefulWidget {
static void show(BuildContext context) {
showDialog(
App app;
int authType;
int? eventKind;
String? authDetail;
AuthDialog({
required this.app,
required this.authType,
this.eventKind,
this.authDetail,
});
static Future<int?> show(BuildContext context, App app, int authType,
{int? eventKind, String? authDetail}) {
return showDialog(
context: context,
builder: (context) {
return AuthDialog();
return AuthDialog(
app: app,
authType: authType,
eventKind: eventKind,
authDetail: authDetail,
);
},
);
}
@@ -31,14 +56,21 @@ class _AuthDialog extends State<AuthDialog> {
);
var hintColor = themeData.hintColor;
// handle this title and des with widget.authType
String authTitle = "Sign Event";
String authDes = "Allow web.nostrmo.com to sign a authenticate event";
authTitle = "AuthType ${widget.authType}";
List<Widget> list = [];
list.add(Container(
margin: baseMargin,
child: Text(
"Allow web.nostrmo.com to sign a authenticate event",
authDes,
),
));
List<Widget> detailList = [];
if (StringUtil.isNotBlank(widget.authDetail)) {
var showDetailWidget = GestureDetector(
onTap: () {
setState(() {
@@ -53,8 +85,6 @@ class _AuthDialog extends State<AuthDialog> {
],
),
);
List<Widget> detailList = [];
if (showDetail) {
detailList.add(Container(
height: 210,
@@ -65,8 +95,7 @@ class _AuthDialog extends State<AuthDialog> {
borderRadius: BorderRadius.circular(10),
),
child: SingleChildScrollView(
child: Text(
"GoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGood"),
child: Text(widget.authDetail!),
),
));
} else {}
@@ -74,6 +103,7 @@ class _AuthDialog extends State<AuthDialog> {
margin: baseMargin,
child: showDetailWidget,
));
}
list.add(Container(
height: 250,
@@ -90,6 +120,16 @@ class _AuthDialog extends State<AuthDialog> {
children: list,
);
return AuthDialogBaseComponnet(title: "Sign Event", child: child);
return AuthDialogBaseComponnet(
app: widget.app,
title: authTitle,
onConfirm: onConfirm,
child: child,
);
}
onConfirm() {
print("auth dialog confirm!");
RouterUtil.back(context, AuthResult.OK);
}
}

View File

@@ -1,16 +1,33 @@
import 'package:flutter/material.dart';
import 'package:nostr_sdk/nip19/nip19.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/const/base.dart';
import 'package:nowser/data/app.dart';
import 'package:nowser/provider/key_provider.dart';
import 'package:nowser/util/router_util.dart';
import 'package:provider/provider.dart';
import '../logo_component.dart';
import 'auth_app_info_component.dart';
class AuthDialogBaseComponnet extends StatefulWidget {
App app;
String title;
Widget child;
AuthDialogBaseComponnet({required this.title, required this.child});
Function onConfirm;
Function(String)? onPubkeyChange;
AuthDialogBaseComponnet({
required this.app,
required this.title,
required this.child,
required this.onConfirm,
this.onPubkeyChange,
});
@override
State<StatefulWidget> createState() {
@@ -30,11 +47,43 @@ class _AuthDialog extends State<AuthDialogBaseComponnet> {
List<Widget> list = [];
var keyWidget =
Selector<KeyProvider, List<String>>(builder: (context, pubkeys, child) {
List<DropdownMenuItem<String>> items = [];
for (var pubkey in pubkeys) {
items.add(DropdownMenuItem(
value: pubkey,
child: Text(
Nip19.encodePubKey(pubkey),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
));
}
if (StringUtil.isBlank(widget.app.pubkey) && pubkeys.isNotEmpty) {
widget.app.pubkey = pubkeys.first;
}
return DropdownButton<String>(
items: items,
onChanged: (String? value) {
if (StringUtil.isNotBlank(value)) {
widget.app.pubkey = value;
setState(() {});
}
},
value: widget.app.pubkey,
);
}, selector: (context, provider) {
return provider.pubkeys;
});
var topWidget = Container(
child: Row(
children: [
Container(
margin: EdgeInsets.only(
margin: const EdgeInsets.only(
left: Base.BASE_PADDING,
right: Base.BASE_PADDING,
top: Base.BASE_PADDING_HALF,
@@ -46,16 +95,7 @@ class _AuthDialog extends State<AuthDialogBaseComponnet> {
child: Container(
margin: EdgeInsets.only(right: Base.BASE_PADDING_HALF),
alignment: Alignment.centerRight,
child: DropdownButton<String>(
items: [
DropdownMenuItem(
child: Text("npubxxxxxx"),
value: "npubxxxxxx",
)
],
onChanged: (Object? value) {},
value: "npubxxxxxx",
),
child: keyWidget,
),
),
],
@@ -76,7 +116,9 @@ class _AuthDialog extends State<AuthDialogBaseComponnet> {
list.add(Container(
margin: baseMargin,
child: AuthAppInfoComponent(),
child: AuthAppInfoComponent(
app: widget.app,
),
));
list.add(Container(
@@ -112,7 +154,12 @@ class _AuthDialog extends State<AuthDialogBaseComponnet> {
margin: EdgeInsets.only(
left: Base.BASE_PADDING_HALF,
),
child: FilledButton(onPressed: () {}, child: Text("Confirm")),
child: FilledButton(
onPressed: () {
widget.onConfirm();
},
child: Text("Confirm"),
),
)
],
),

View File

@@ -64,7 +64,7 @@ class _WebHomeComponent extends State<WebHomeComponent> {
RouterUtil.router(context, RouterPath.WEB_TABS);
}),
wrapBottomBtn(const Icon(Icons.space_dashboard), onTap: () {
AuthDialog.show(context);
// AuthDialog.show(context);
// AuthAppConnectDialog.show(context);
}),
wrapBottomBtn(const Icon(Icons.segment), onTap: () {

View File

@@ -1,6 +1,19 @@
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:nostr_sdk/event.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/component/webview/web_info.dart';
import 'package:nowser/const/app_type.dart';
import 'package:nowser/const/auth_type.dart';
import 'package:nowser/main.dart';
import 'package:nowser/util/permission_check_mixin.dart';
import '../../const/auth_result.dart';
import '../../data/app.dart';
class WebViewComponent extends StatefulWidget {
WebInfo webInfo;
@@ -21,23 +34,479 @@ class WebViewComponent extends StatefulWidget {
}
}
class _WebViewComponent extends State<WebViewComponent> {
class _WebViewComponent extends State<WebViewComponent>
with PermissionCheckMixin {
InAppWebViewController? webViewController;
late ContextMenu contextMenu;
InAppWebViewSettings settings = InAppWebViewSettings(
isInspectable: kDebugMode,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
iframeAllow: "camera; microphone",
iframeAllowFullscreen: true);
PullToRefreshController? pullToRefreshController;
double progress = 0;
@override
void initState() {
super.initState();
contextMenu = ContextMenu(
menuItems: [
ContextMenuItem(
id: 1,
title: "Special",
action: () async {
print("Menu item Special clicked!");
print(await webViewController?.getSelectedText());
await webViewController?.clearFocus();
})
],
settings: ContextMenuSettings(hideDefaultSystemContextMenuItems: false),
onCreateContextMenu: (hitTestResult) async {
print("onCreateContextMenu");
print(hitTestResult.extra);
print(await webViewController?.getSelectedText());
},
onHideContextMenu: () {
print("onHideContextMenu");
},
onContextMenuActionItemClicked: (contextMenuItemClicked) async {
var id = contextMenuItemClicked.id;
print("onContextMenuActionItemClicked: " +
id.toString() +
" " +
contextMenuItemClicked.title);
});
pullToRefreshController = kIsWeb ||
![TargetPlatform.iOS, TargetPlatform.android]
.contains(defaultTargetPlatform)
? null
: PullToRefreshController(
settings: PullToRefreshSettings(
color: Colors.blue,
),
onRefresh: () async {
if (defaultTargetPlatform == TargetPlatform.android) {
webViewController?.reload();
} else if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
webViewController?.loadUrl(
urlRequest:
URLRequest(url: await webViewController?.getUrl()));
}
},
);
}
@override
Widget build(BuildContext context) {
return Container(
child: InAppWebView(
child: Stack(
children: [
InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(widget.webInfo.url)),
initialUserScripts: UnmodifiableListView<UserScript>([]),
initialSettings: settings,
contextMenu: contextMenu,
pullToRefreshController: pullToRefreshController,
onWebViewCreated: (controller) async {
webViewController = controller;
// initJSHandle(controller);
initJSHandle(controller);
widget.onWebViewCreated(widget.webInfo, controller);
},
onTitleChanged: (controller, title) {
widget.onTitleChanged(widget.webInfo, controller, title);
},
onLoadStart: (controller, url) async {},
onPermissionRequest: (controller, request) async {
return PermissionResponse(
resources: request.resources,
action: PermissionResponseAction.GRANT);
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
// var uri = navigationAction.request.url!;
// if (uri.scheme == "lightning" &&
// StringUtil.isNotBlank(uri.path)) {
// var result =
// await NIP07Dialog.show(context, NIP07Methods.lightning);
// if (result == true) {
// await LightningUtil.goToPay(context, uri.path);
// }
// return NavigationActionPolicy.CANCEL;
// }
// if (uri.scheme == "nostr+walletconnect") {
// webViewProvider.closeAndReturn(uri.toString());
// return NavigationActionPolicy.CANCEL;
// }
// if (![
// "http",
// "https",
// "file",
// "chrome",
// "data",
// "javascript",
// "about"
// ].contains(uri.scheme)) {
// if (await canLaunchUrl(uri)) {
// // Launch the App
// await launchUrl(
// uri,
// );
// // and cancel the request
// return NavigationActionPolicy.CANCEL;
// }
// }
return NavigationActionPolicy.ALLOW;
},
onLoadStop: (controller, url) async {
pullToRefreshController?.endRefreshing();
addInitScript(controller);
},
onReceivedError: (controller, request, error) {
pullToRefreshController?.endRefreshing();
},
onProgressChanged: (controller, progress) {
if (progress == 100) {
pullToRefreshController?.endRefreshing();
}
setState(() {
this.progress = progress / 100;
});
},
onUpdateVisitedHistory: (controller, url, isReload) {},
onConsoleMessage: (controller, consoleMessage) {
print(consoleMessage);
},
),
progress < 1.0
? LinearProgressIndicator(value: progress)
: Container(),
],
),
);
}
Future<void> nip07Reject(String resultId, String contnet) async {
var script = "window.nostr.reject(\"$resultId\", \"${contnet}\");";
await webViewController!.evaluateJavascript(source: script);
}
void initJSHandle(InAppWebViewController controller) {
controller.addJavaScriptHandler(
handlerName: "Nowser_JS_getPublicKey",
callback: (jsMsgs) async {
var jsMsg = jsMsgs[0];
// print("Nowser_JS_getPublicKey $jsMsg");
var jsonObj = jsonDecode(jsMsg);
var resultId = jsonObj["resultId"];
String? code = await getCode();
if (code == null) {
return;
}
checkPermission(context, AppType.WEB, code, AuthType.GET_PUBLIC_KEY,
() {
nip07Reject(resultId, "Forbid");
}, (app, signer) {
var pubkey = app.pubkey;
var script = "window.nostr.callback(\"$resultId\", \"$pubkey\");";
controller.evaluateJavascript(source: script);
});
},
);
controller.addJavaScriptHandler(
handlerName: "Nowser_JS_signEvent",
callback: (jsMsgs) async {
var jsMsg = jsMsgs[0];
// print("Nowser_JS_signEvent $jsMsg");
var jsonObj = jsonDecode(jsMsg);
var resultId = jsonObj["resultId"];
var content = jsonObj["msg"];
String? code = await getCode();
if (code == null) {
return;
}
try {
var eventObj = jsonDecode(content);
var eventKind = eventObj["kind"];
if (eventKind is int) {
checkPermission(context, AppType.WEB, code, AuthType.SIGN_EVENT,
eventKind: eventKind, () {
nip07Reject(resultId, "Forbid");
}, (app, signer) async {
var tags = eventObj["tags"];
Event? event = Event(app.pubkey!, eventObj["kind"], tags ?? [],
eventObj["content"]);
event = await signer.signEvent(event);
if (event == null) {
return;
}
var eventResultStr = jsonEncode(event.toJson());
// TODO this method to handle " may be error
eventResultStr = eventResultStr.replaceAll("\"", "\\\"");
var script =
"window.nostr.callback(\"$resultId\", JSON.parse(\"$eventResultStr\"));";
webViewController!.evaluateJavascript(source: script);
});
}
} catch (e) {
nip07Reject(resultId, "Sign fail");
}
},
);
controller.addJavaScriptHandler(
handlerName: "Nowser_JS_getRelays",
callback: (jsMsgs) async {
var jsMsg = jsMsgs[0];
// print("Nowser_JS_getRelays $jsMsg");
var jsonObj = jsonDecode(jsMsg);
var resultId = jsonObj["resultId"];
String? code = await getCode();
if (code == null) {
return;
}
checkPermission(context, AppType.WEB, code, AuthType.GET_RELAYS, () {
nip07Reject(resultId, "Forbid");
}, (app, signer) {
// TODO handle getRelays
// var app = appProvider.getApp(AppType.WEB, code);
// if (app != null) {
// var relayMaps = {};
// var relayAddrs = relayProvider.relayAddrs;
// for (var relayAddr in relayAddrs) {
// relayMaps[relayAddr] = {"read": true, "write": true};
// }
// var resultStr = jsonEncode(relayMaps);
// resultStr = resultStr.replaceAll("\"", "\\\"");
// var script =
// "window.nostr.callback(\"$resultId\", JSON.parse(\"$resultStr\"));";
// webViewController!.evaluateJavascript(source: script);
// }
});
},
);
controller.addJavaScriptHandler(
handlerName: "Nowser_JS_nip04_encrypt",
callback: (jsMsgs) async {
var jsMsg = jsMsgs[0];
// print("Nowser_JS_nip04_encrypt $jsMsg");
var jsonObj = jsonDecode(jsMsg);
var resultId = jsonObj["resultId"];
var msg = jsonObj["msg"];
if (msg != null && msg is Map) {
var pubkey = msg["pubkey"];
var plaintext = msg["plaintext"];
String? code = await getCode();
if (code == null) {
return;
}
checkPermission(context, AppType.WEB, code, AuthType.NIP04_ENCRYPT,
() {
nip07Reject(resultId, "Forbid");
}, (app, signer) async {
var resultStr = await signer.encrypt(pubkey, plaintext);
if (StringUtil.isBlank(resultStr)) {
return;
}
var script =
"window.nostr.callback(\"$resultId\", \"$resultStr\");";
webViewController!.evaluateJavascript(source: script);
});
}
},
);
controller.addJavaScriptHandler(
handlerName: "Nowser_JS_nip04_decrypt",
callback: (jsMsgs) async {
var jsMsg = jsMsgs[0];
// print("Nowser_JS_nip04_decrypt $jsMsg");
var jsonObj = jsonDecode(jsMsg.message);
var resultId = jsonObj["resultId"];
var msg = jsonObj["msg"];
if (msg != null && msg is Map) {
var pubkey = msg["pubkey"];
var ciphertext = msg["ciphertext"];
String? code = await getCode();
if (code == null) {
return;
}
checkPermission(context, AppType.WEB, code, AuthType.NIP04_DECRYPT,
() {
nip07Reject(resultId, "Forbid");
}, (app, signer) async {
var app = appProvider.getApp(AppType.WEB, code);
if (app != null) {
var resultStr = await signer.decrypt(pubkey, ciphertext);
if (StringUtil.isBlank(resultStr)) {
return;
}
var script =
"window.nostr.callback(\"$resultId\", \"$resultStr\");";
webViewController!.evaluateJavascript(source: script);
}
});
}
},
);
controller.addJavaScriptHandler(
handlerName: "Nowser_JS_nip44_encrypt",
callback: (jsMsgs) async {
var jsMsg = jsMsgs[0];
// print("Nowser_JS_nip04_encrypt $jsMsg");
var jsonObj = jsonDecode(jsMsg);
var resultId = jsonObj["resultId"];
var msg = jsonObj["msg"];
if (msg != null && msg is Map) {
var pubkey = msg["pubkey"];
var plaintext = msg["plaintext"];
String? code = await getCode();
if (code == null) {
return;
}
checkPermission(context, AppType.WEB, code, AuthType.NIP44_ENCRYPT,
() {
nip07Reject(resultId, "Forbid");
}, (app, signer) async {
var resultStr = await signer.nip44Encrypt(pubkey, plaintext);
if (StringUtil.isBlank(resultStr)) {
return;
}
var script =
"window.nostr.callback(\"$resultId\", \"$resultStr\");";
webViewController!.evaluateJavascript(source: script);
});
}
},
);
controller.addJavaScriptHandler(
handlerName: "Nowser_JS_nip44_decrypt",
callback: (jsMsgs) async {
var jsMsg = jsMsgs[0];
// print("Nowser_JS_nip04_decrypt $jsMsg");
var jsonObj = jsonDecode(jsMsg.message);
var resultId = jsonObj["resultId"];
var msg = jsonObj["msg"];
if (msg != null && msg is Map) {
var pubkey = msg["pubkey"];
var ciphertext = msg["ciphertext"];
String? code = await getCode();
if (code == null) {
return;
}
checkPermission(context, AppType.WEB, code, AuthType.NIP44_DECRYPT,
() {
nip07Reject(resultId, "Forbid");
}, (app, signer) async {
var resultStr = await signer.nip44Decrypt(pubkey, ciphertext);
if (StringUtil.isBlank(resultStr)) {
return;
}
var script =
"window.nostr.callback(\"$resultId\", \"$resultStr\");";
webViewController!.evaluateJavascript(source: script);
});
}
},
);
}
void addInitScript(InAppWebViewController controller) {
controller.evaluateJavascript(source: """
window.nostr = {
_call(channel, message) {
return new Promise((resolve, reject) => {
var resultId = "callbackResult_" + Math.floor(Math.random() * 100000000);
var arg = {"resultId": resultId};
if (message) {
arg["msg"] = message;
}
var argStr = JSON.stringify(arg);
window.flutter_inappwebview
.callHandler(channel, argStr);
window.nostr._requests[resultId] = {resolve, reject}
});
},
_requests: {},
callback(resultId, message) {
window.nostr._requests[resultId].resolve(message);
},
reject(resultId, message) {
window.nostr._requests[resultId].reject(message);
},
async getPublicKey() {
return window.nostr._call("Nowser_JS_getPublicKey");
},
async signEvent(event) {
return window.nostr._call("Nowser_JS_signEvent", JSON.stringify(event));
},
async getRelays() {
return window.nostr._call("Nowser_JS_getRelays");
},
nip04: {
async encrypt(pubkey, plaintext) {
return window.nostr._call("Nowser_JS_nip04_encrypt", {"pubkey": pubkey, "plaintext": plaintext});
},
async decrypt(pubkey, ciphertext) {
return window.nostr._call("Nowser_JS_nip04_decrypt", {"pubkey": pubkey, "ciphertext": ciphertext});
},
},
nip44: {
async encrypt(pubkey, plaintext) {
return window.nostr._call("Nowser_JS_nip44_encrypt", {"pubkey": pubkey, "plaintext": plaintext});
},
async decrypt(pubkey, ciphertext) {
return window.nostr._call("Nowser_JS_nip44_decrypt", {"pubkey": pubkey, "ciphertext": ciphertext});
},
},
};
""");
}
Future<String?> getCode() async {
if (webViewController != null) {
var url = await webViewController!.getUrl();
if (url != null) {
return url.host;
}
}
return null;
}
@override
Future<App> getApp(int appType, String code) async {
String? name;
String? image;
if (webViewController != null) {
name = await webViewController!.getTitle();
var favicons = await webViewController!.getFavicons();
if (favicons.isNotEmpty) {
image = favicons.first.url.toString();
}
}
return App(appType: appType, code: code, name: name, image: image);
}
}

View File

@@ -1,5 +1,7 @@
class AuthResult {
static const int OK = 1;
static const int CANCEL = -1;
static const int REJECT = -1;
static const int ASK = 0;
}

View File

@@ -0,0 +1,3 @@
class ReasonablePermissions {
static const String text = "1;3;4;5;6;7,2-22242";
}

5
lib/const/zap_type.dart Normal file
View File

@@ -0,0 +1,5 @@
class ZapType {
static const int LIGHTNING_APP = 1;
static const int NWC = 2;
}

View File

@@ -1,25 +1,66 @@
class App {
int id;
int? id;
String pubkey;
String? pubkey;
int appType;
int? appType;
String code;
String? code;
String name;
String? name;
String? image;
String? permissions;
int? connectType;
App({
required this.id,
required this.pubkey,
required this.appType,
required this.code,
required this.name,
String? alwaysAllow;
String? alwaysReject;
int? createdAt;
int? updatedAt;
App(
{this.id,
this.pubkey,
this.appType,
this.code,
this.name,
this.image,
this.permissions,
});
this.connectType,
this.alwaysAllow,
this.alwaysReject,
this.createdAt,
this.updatedAt});
App.fromJson(Map<String, dynamic> json) {
id = json['id'];
pubkey = json['pubkey'];
appType = json['app_type'];
code = json['code'];
name = json['name'];
image = json['image'];
connectType = json['connect_type'];
alwaysAllow = json['always_allow'];
alwaysReject = json['always_reject'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['pubkey'] = this.pubkey;
data['app_type'] = this.appType;
data['code'] = this.code;
data['name'] = this.name;
data['image'] = this.image;
data['connect_type'] = this.connectType;
data['always_allow'] = this.alwaysAllow;
data['always_reject'] = this.alwaysReject;
data['created_at'] = this.createdAt;
data['updated_at'] = this.updatedAt;
return data;
}
}

View File

@@ -1 +1,41 @@
class AppDB {}
import 'package:nowser/data/db.dart';
import 'package:sqflite/sqflite.dart';
import 'app.dart';
class AppDB {
static Future<List<App>> all() async {
List<App> objs = [];
var db = await DB.getCurrentDatabase();
List<Map<String, dynamic>> list = await db.rawQuery("select * from app");
for (var i = 0; i < list.length; i++) {
var json = list[i];
objs.add(App.fromJson(json));
}
return objs;
}
static Future<App?> get(int id, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
var list = await db.query("app", where: "id = ?", whereArgs: [id]);
if (list.isNotEmpty) {
return App.fromJson(list[0]);
}
}
static Future<int> insert(App o, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
return await db.insert("app", o.toJson());
}
static Future update(App o, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
await db.update("app", o.toJson(), where: "id = ?", whereArgs: [o.pubkey]);
}
static Future<void> delete(int id, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
db.execute("delete from app where id = ?");
}
}

View File

@@ -1,9 +1,9 @@
class AuthLog {
int id;
int? id;
int appId;
int? appId;
int authType;
int? authType;
int? eventKind;
@@ -11,18 +11,41 @@ class AuthLog {
String? content;
int authResult;
int? authResult;
int createdAt;
int? createdAt;
AuthLog({
required this.id,
required this.appId,
required this.authType,
AuthLog(
{this.id,
this.appId,
this.authType,
this.eventKind,
this.title,
this.content,
required this.authResult,
required this.createdAt,
});
this.authResult,
this.createdAt});
AuthLog.fromJson(Map<String, dynamic> json) {
id = json['id'];
appId = json['app_id'];
authType = json['auth_type'];
eventKind = json['event_kind'];
title = json['title'];
content = json['content'];
authResult = json['auth_result'];
createdAt = json['created_at'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['app_id'] = this.appId;
data['auth_type'] = this.authType;
data['event_kind'] = this.eventKind;
data['title'] = this.title;
data['content'] = this.content;
data['auth_result'] = this.authResult;
data['created_at'] = this.createdAt;
return data;
}
}

View File

@@ -36,10 +36,13 @@ class DB {
static Future<void> _onCreate(Database db, int version) async {
// init db
db.execute(
"create table app(id integer not null constraint app_pk primary key autoincrement,pubkey text not null,app_type integer not null,code text not null,name text not null,image text,permissions text);");
"create table app(id integer not null constraint app_pk primary key autoincrement,pubkey text not null,app_type integer not null,code text not null,name text,image text,connect_type integer not null,always_allow text,always_reject text,created_at integer not null,updated_at integer not null);");
db.execute(
"create table auth_log(id integer not null constraint auth_log_pk primary key autoincrement,app_id integer not null,auth_type integer not null,event_kind integer,title text,content text,auth_result integer not null,created_at integer not null);");
db.execute("create index auth_log_index on auth_log (app_id);");
db.execute(
"create table zap_log(id integer not null constraint zap_log_pk primary key autoincrement,app_id integer not null constraint zap_log_index unique,zap_type integer not null,num integer not null,created_at integer not null);");
db.execute("create index zap_log_index on zap_log (app_id);");
}
static Future<Database> getCurrentDatabase() async {

37
lib/data/zap_log.dart Normal file
View File

@@ -0,0 +1,37 @@
class ZapLog {
int? id;
int? appId;
int? zapType;
int? num;
int? createdAt;
ZapLog({
this.id,
this.appId,
this.zapType,
this.num,
this.createdAt,
});
ZapLog.fromJson(Map<String, dynamic> json) {
id = json['id'];
appId = json['app_id'];
zapType = json['zap_type'];
num = json['num'];
createdAt = json['created_at'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['app_id'] = this.appId;
data['zap_type'] = this.zapType;
data['num'] = this.num;
data['created_at'] = this.createdAt;
return data;
}
}

1
lib/data/zap_log_db.dart Normal file
View File

@@ -0,0 +1 @@
class ZapLogDB {}

View File

@@ -8,6 +8,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/data/db.dart';
import 'package:nowser/provider/app_provider.dart';
import 'package:nowser/provider/key_provider.dart';
import 'package:nowser/provider/web_provider.dart';
import 'package:nowser/router/index/index_router.dart';
@@ -31,12 +32,15 @@ late SettingProvider settingProvider;
late KeyProvider keyProvider;
late AppProvider appProvider;
late Map<String, WidgetBuilder> routes;
Future<void> main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
keyProvider = KeyProvider();
appProvider = AppProvider();
var dataUtilTask = DataUtil.getInstance();
var keyTask = keyProvider.init();
@@ -44,7 +48,8 @@ Future<void> main() async {
var dataFutureResultList = await Future.wait([dataUtilTask, keyTask, dbTask]);
var settingTask = SettingProvider.getInstance();
var futureResultList = await Future.wait([settingTask]);
var appTask = appProvider.reload();
var futureResultList = await Future.wait([settingTask, appTask]);
settingProvider = futureResultList[0] as SettingProvider;
webProvider = WebProvider();

View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/const/connect_type.dart';
import 'package:nowser/data/app_db.dart';
import '../const/auth_result.dart';
import '../data/app.dart';
class AppProvider extends ChangeNotifier {
List<App> _list = [];
Map<String, Map<String, int>> appPermissions = {};
Future<void> reload() async {
appPermissions = {};
var allApp = await AppDB.all();
_list = allApp;
notifyListeners();
}
Future<void> add(App app) async {
if (await AppDB.insert(app) > 0) {
_list.add(app);
notifyListeners();
}
}
String getAppCode(int appType, String code) {
return "${appType}_$code";
}
int checkPermission(int appType, String code, int authType,
{int? eventKind}) {
var app = getApp(appType, code);
if (app != null) {
if (app.connectType == ConnectType.FULLY_TRUST) {
return AuthResult.OK;
} else if (app.connectType == ConnectType.ALWAY_REJECT) {
return AuthResult.REJECT;
} else {
var appCode = getAppCode(appType, code);
var permissionsMap = appPermissions[appCode];
if (permissionsMap == null) {
permissionsMap = _getPermissionMap(app);
appPermissions[appCode] = permissionsMap;
}
var key = "$authType";
if (eventKind != null) {
key = "$key-$eventKind";
}
var value = permissionsMap[key];
if (value != null) {
return value;
}
}
}
return AuthResult.ASK;
}
Map<String, int> _getPermissionMap(App app) {
Map<String, int> m = {};
_putPermissionMapValue(m, app.alwaysAllow, 1);
_putPermissionMapValue(m, app.alwaysReject, -1);
return m;
}
void _putPermissionMapValue(
Map<String, int> m, String? permissionText, int value) {
if (StringUtil.isNotBlank(permissionText)) {
var permissionStrs = permissionText!.split(";");
for (var permissionStr in permissionStrs) {
var strs = permissionStr.split("-");
var kindStr = strs[0];
if (strs.length == 1) {
m[kindStr] = value;
} else if (strs.length > 1) {
var eventKindsStr = strs[1];
var eventKindStrs = eventKindsStr.split(",");
for (var eventKindStr in eventKindStrs) {
var key = "$kindStr-$eventKindStr";
m[key] = value;
}
}
}
}
}
App? getApp(int appType, String code) {
for (var app in _list) {
if (app.appType == appType && app.code == code) {
return app;
}
}
return null;
}
}

View File

@@ -3,6 +3,8 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:nostr_sdk/client_utils/keys.dart';
import 'package:nostr_sdk/signer/local_nostr_signer.dart';
import 'package:nostr_sdk/signer/nostr_signer.dart';
import 'package:nostr_sdk/utils/string_util.dart';
class KeyProvider extends ChangeNotifier {
@@ -104,4 +106,13 @@ class KeyProvider extends ChangeNotifier {
bool exist(String privateKey) {
return keys.contains(privateKey);
}
NostrSigner? getSigner(String pubkey) {
var key = keysMap[pubkey];
if (StringUtil.isNotBlank(key)) {
return LocalNostrSigner(key!);
}
return null;
}
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:nostr_sdk/signer/nostr_signer.dart';
import 'package:nowser/component/auth_dialog/auth_app_connect_dialog.dart';
import 'package:nowser/component/auth_dialog/auth_dialog.dart';
import 'package:nowser/const/auth_result.dart';
import 'package:nowser/data/auth_log.dart';
import 'package:nowser/main.dart';
import '../const/connect_type.dart';
import '../data/app.dart';
mixin PermissionCheckMixin {
Future<void> checkPermission(BuildContext context, int appType, String code,
int authType, Function reject, Function(App, NostrSigner) confirm,
{int? eventKind, String? authDetail}) async {
var app = appProvider.getApp(appType, code);
if (app == null) {
// app is null, app connect
var newApp = await getApp(appType, code);
await AuthAppConnectDialog.show(context, newApp);
// reload from provider
app = appProvider.getApp(appType, code);
}
if (app == null) {
// not allow connect
reject();
return;
}
var signer = getSigner(app.pubkey!);
if (signer == null) {
reject();
return;
}
if (app.connectType == ConnectType.FULLY_TRUST) {
saveAuthLog(app, authType, eventKind, authDetail, AuthResult.OK);
confirm(app, signer);
return;
} else if (app.connectType == ConnectType.REASONABLE) {
var permissionCheckResult = appProvider
.checkPermission(appType, code, authType, eventKind: eventKind);
print("permissionCheckResult $permissionCheckResult");
if (permissionCheckResult == AuthResult.OK) {
saveAuthLog(app, authType, eventKind, authDetail, AuthResult.OK);
confirm(app, signer);
return;
} else if (permissionCheckResult == AuthResult.REJECT) {
saveAuthLog(app, authType, eventKind, authDetail, AuthResult.REJECT);
reject();
return;
}
var authResult = await AuthDialog.show(context, app, authType,
eventKind: eventKind, authDetail: authDetail);
if (authResult == AuthResult.OK) {
saveAuthLog(app, authType, eventKind, authDetail, AuthResult.OK);
confirm(app, signer);
return;
}
}
saveAuthLog(app, authType, eventKind, authDetail, AuthResult.REJECT);
reject();
return;
}
void saveAuthLog(App app, int authType, int? eventKind, String? authDetail,
int authResult) {
// TODO
}
// this method should override
Future<App> getApp(int appType, String code) async {
// TODO name, image
return App(appType: appType, code: code);
}
NostrSigner? getSigner(String pubkey) {
return keyProvider.getSigner(pubkey);
}
}

View File

@@ -33,9 +33,9 @@ dependencies:
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.6
nostr_sdk:
path: packages/nostr_sdk
receive_intent: ^0.2.5
provider: ^6.1.2
flutter_inappwebview: ^6.0.0