diff --git a/.gitignore b/.gitignore index a36b221..9cd7f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ doc/api/ *.db lib_old/ +sessions/ diff --git a/Makefile b/Makefile index e7ae943..39d38b2 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,9 @@ tests: ## Run unit tests run: ## Run dev project docker compose up --build +upgrade: ## Run dart pub upgrade + dart pub upgrade + fix-project: fix analyze format ## Fix whole project check-project: fix-project tests ## Run all checks diff --git a/bin/running_on_dart.dart b/bin/running_on_dart.dart index 2b6206f..421d97e 100644 --- a/bin/running_on_dart.dart +++ b/bin/running_on_dart.dart @@ -43,8 +43,11 @@ void main() async { IgnoreExceptions(), commands, pagination, + SessionManagerPlugin(), ], )); await setupContainer(client); + + WebServer().startServer(); } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 3089fe2..e4bbf49 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,14 +1,17 @@ version: '3.9' services: running_on_dart: - image: ghcr.io/nyxx-discord/running_on_dart:4.9.3 + image: ghcr.io/nyxx-discord/running_on_dart:4.10.0-dev.7 container_name: running_on_dart env_file: - .env - links: - - db depends_on: - db + volumes: + - "rod_sessions:/sessions" + networks: + - internal + - nginx-proxy_default db: image: postgres:17 @@ -18,6 +21,15 @@ services: - .env volumes: - "rod_db_17:/var/lib/postgresql/data" + networks: + - internal volumes: rod_db_17: + rod_sessions: + +networks: + nginx-proxy_default: + name: nginx-proxy_default + external: true + internal: diff --git a/docker-compose.yml b/docker-compose.yml index e831fe4..1f53a55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,10 +6,13 @@ services: container_name: running_on_dart env_file: - .env - links: - - db + ports: + - "8088:8088" depends_on: - db + volumes: + - "./sessions:/sessions" + - "./templates:/templates" db: image: postgres:17 diff --git a/lib/running_on_dart.dart b/lib/running_on_dart.dart index 2dc8d04..dc54e15 100644 --- a/lib/running_on_dart.dart +++ b/lib/running_on_dart.dart @@ -15,3 +15,6 @@ export 'src/settings.dart'; export 'src/converter.dart'; export 'src/error_handler.dart'; export 'src/init.dart'; + +export 'src/web_app/api_server.dart' show WebServer; +export 'src/web_app/session_manager_plugin.dart' show SessionManagerPlugin; diff --git a/lib/src/commands/info.dart b/lib/src/commands/info.dart index 7369b19..36c0cba 100644 --- a/lib/src/commands/info.dart +++ b/lib/src/commands/info.dart @@ -2,71 +2,43 @@ import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:nyxx_extensions/nyxx_extensions.dart'; -import 'package:running_on_dart/src/modules/bot_start_duration.dart'; -import 'package:running_on_dart/src/modules/docs.dart'; -import 'package:running_on_dart/src/modules/reminder.dart'; -import 'package:running_on_dart/src/modules/tag.dart'; -import 'package:running_on_dart/src/settings.dart'; +import 'package:running_on_dart/src/services/bot_info.dart'; import 'package:running_on_dart/src/util/util.dart'; final info = ChatCommand( 'info', 'Get info about the bot', id('info', (ChatContext context) async { - final color = getRandomColor(); final currentUser = await context.client.user.get(); + final botInfo = await Injector.appInstance.get().getCurrentBotInfo(); - final startDate = Injector.appInstance.get().startDate; final startDateStr = - "${startDate.format(TimestampStyle.longDateTime)} (${startDate.format(TimestampStyle.relativeTime)})"; - - final docsUpdatedDate = Injector.appInstance.get().lastUpdate; - final docsUpdateStr = docsUpdatedDate != null - ? "${docsUpdatedDate.format(TimestampStyle.longDateTime)} (${docsUpdatedDate.format(TimestampStyle.relativeTime)})" + "${botInfo.uptime.format(TimestampStyle.longDateTime)} (${botInfo.uptime.format(TimestampStyle.relativeTime)})"; + final docsUpdateStr = botInfo.docsUpdate != null + ? "${botInfo.docsUpdate!.format(TimestampStyle.longDateTime)} (${botInfo.docsUpdate!.format(TimestampStyle.relativeTime)})" : "Never"; final embed = EmbedBuilder( - color: color, + color: getRandomColor(), author: EmbedAuthorBuilder( name: currentUser.username, iconUrl: currentUser.avatar.url, url: Uri.parse(ApiOptions.nyxxRepositoryUrl), ), footer: EmbedFooterBuilder( - text: 'nyxx ${ApiOptions.nyxxVersion}' - ' | ROD $version' - ' | Dart SDK ${getDartPlatform()}'), + text: 'nyxx ${botInfo.nyxxVersion}' + ' | ROD ${botInfo.version}' + ' | Dart SDK ${botInfo.dartPlatform}'), fields: [ - EmbedFieldBuilder(name: 'Cached guilds', value: context.client.guilds.cache.length.toString(), isInline: true), - EmbedFieldBuilder(name: 'Cached users', value: context.client.users.cache.length.toString(), isInline: true), - EmbedFieldBuilder( - name: 'Cached channels', value: context.client.channels.cache.length.toString(), isInline: true), - EmbedFieldBuilder( - name: 'Cached voice states', - value: context.client.guilds.cache.values - .map((g) => g.voiceStates.length) - .fold(0, (value, element) => value + element) - .toString(), - isInline: true), - EmbedFieldBuilder(name: 'Shard count', value: context.client.gateway.shards.length.toString(), isInline: true), - EmbedFieldBuilder( - name: 'Cached messages', - value: context.client.channels.cache.values - .whereType() - .map((c) => c.messages.cache.length) - .fold(0, (value, element) => value + element) - .toString(), - isInline: true), - EmbedFieldBuilder(name: 'Memory usage (current/RSS)', value: getCurrentMemoryString(), isInline: true), - EmbedFieldBuilder( - name: 'Tags in guild', - value: - Injector.appInstance.get().countCachedTags(context.guild?.id ?? context.user.id).toString(), - isInline: true), - EmbedFieldBuilder( - name: 'Current reminders', - value: Injector.appInstance.get().reminders.length.toString(), - isInline: true), + EmbedFieldBuilder(name: 'Cached guilds', value: botInfo.cachedGuilds.toString(), isInline: true), + EmbedFieldBuilder(name: 'Cached users', value: botInfo.cachedUsers.toString(), isInline: true), + EmbedFieldBuilder(name: 'Cached channels', value: botInfo.cachedChannels.toString(), isInline: true), + EmbedFieldBuilder(name: 'Cached voice states', value: botInfo.cachedVoiceStates.toString(), isInline: true), + EmbedFieldBuilder(name: 'Shard count', value: botInfo.shardCount.toString(), isInline: true), + EmbedFieldBuilder(name: 'Cached messages', value: botInfo.cachedMessages.toString(), isInline: true), + EmbedFieldBuilder(name: 'Memory usage (current/RSS)', value: botInfo.memoryUserString, isInline: true), + EmbedFieldBuilder(name: 'Tags in guild', value: botInfo.totalTagsCount.toString(), isInline: true), + EmbedFieldBuilder(name: 'Current reminders', value: botInfo.totalRemainderCount.toString(), isInline: true), EmbedFieldBuilder(name: 'Uptime', value: startDateStr, isInline: false), EmbedFieldBuilder(name: 'Docs Update', value: docsUpdateStr, isInline: false), ], diff --git a/lib/src/init.dart b/lib/src/init.dart index 485ffe1..0a9ac16 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -16,6 +16,7 @@ import 'package:running_on_dart/src/repository/jellyfin_config.dart'; import 'package:running_on_dart/src/repository/kavita.dart'; import 'package:running_on_dart/src/repository/reminder.dart'; import 'package:running_on_dart/src/repository/tag.dart'; +import 'package:running_on_dart/src/services/bot_info.dart'; import 'package:running_on_dart/src/services/db.dart'; import 'package:running_on_dart/src/modules/feature_settings.dart'; @@ -39,7 +40,8 @@ Future setupContainer(NyxxGateway client) async { ..registerSingleton(() => JellyfinModuleV2()) ..registerSingleton(() => MentionsMonitoringModule()) ..registerSingleton(() => KavitaModule()) - ..registerSingleton(() => EmojiReactModule()); + ..registerSingleton(() => EmojiReactModule()) + ..registerSingleton(() => BotInfoService()); await Injector.appInstance.get().init(); await Injector.appInstance.get().init(); diff --git a/lib/src/modules/tag.dart b/lib/src/modules/tag.dart index 98daf64..b9f89ab 100644 --- a/lib/src/modules/tag.dart +++ b/lib/src/modules/tag.dart @@ -38,6 +38,7 @@ class TagModule implements RequiresInitialization { } int countCachedTags(Snowflake targetId) => tags.where((tag) => tag.guildId == targetId).length; + int countTags() => tags.length; /// Get all the enabled tags in a guild. Iterable getGuildTags(Snowflake guildId) => tags.where((tag) => tag.guildId == guildId && tag.enabled); diff --git a/lib/src/services/bot_info.dart b/lib/src/services/bot_info.dart new file mode 100644 index 0000000..1950f13 --- /dev/null +++ b/lib/src/services/bot_info.dart @@ -0,0 +1,96 @@ +import 'package:injector/injector.dart'; +import 'package:nyxx/nyxx.dart'; +import 'package:running_on_dart/src/modules/bot_start_duration.dart'; +import 'package:running_on_dart/src/modules/docs.dart'; +import 'package:running_on_dart/src/modules/reminder.dart'; +import 'package:running_on_dart/src/modules/tag.dart'; + +import 'package:running_on_dart/src/settings.dart' as settings; +import 'package:running_on_dart/src/util/util.dart'; + +class BotInfo { + String get nyxxVersion => ApiOptions.nyxxVersion; + String get version => settings.version; + String get dartPlatform => getDartPlatform(); + String get memoryUserString => getCurrentMemoryString(); + + final int cachedGuilds; + final int cachedUsers; + final int cachedChannels; + final int cachedVoiceStates; + final int shardCount; + final int cachedMessages; + final int totalTagsCount; + final int totalRemainderCount; + final DateTime uptime; + final DateTime? docsUpdate; + + BotInfo( + {required this.cachedGuilds, + required this.cachedUsers, + required this.cachedChannels, + required this.cachedVoiceStates, + required this.shardCount, + required this.cachedMessages, + required this.totalTagsCount, + required this.totalRemainderCount, + required this.uptime, + required this.docsUpdate}); + + Map toJson() => { + 'nyxx_version': nyxxVersion, + 'version': version, + 'platform': dartPlatform, + 'memory_usage_string': memoryUserString, + 'cached_channels': cachedChannels, + 'cached_messages': cachedMessages, + 'cached_guilds': cachedGuilds, + 'cached_users': cachedUsers, + 'cached_voice_states': cachedVoiceStates, + 'shard_count': shardCount, + 'total_tags_count': totalTagsCount, + 'total_reminder_count': totalRemainderCount, + 'uptime': uptime.toIso8601String(), + 'docs_update': docsUpdate?.toIso8601String(), + }; +} + +class BotInfoService { + final NyxxGateway client = Injector.appInstance.get(); + final TagModule tagModule = Injector.appInstance.get(); + final ReminderModule reminderModule = Injector.appInstance.get(); + final BotStartDuration startDurationModule = Injector.appInstance.get(); + final DocsModule docsModule = Injector.appInstance.get(); + + Future getCurrentBotInfo() async { + final cachedGuilds = client.guilds.cache.length; + final cachedUsers = client.users.cache.length; + final cachedChannels = client.channels.cache.length; + final cachedVoiceStates = client.guilds.cache.values + .map((g) => g.voiceStates.length) + .fold(0, (value, element) => value + element) + .ceil(); + final shardCount = client.gateway.shards.length; + final cachedMessages = client.channels.cache.values + .whereType() + .map((c) => c.messages.cache.length) + .fold(0, (value, element) => value + element) + .ceil(); + final totalTags = tagModule.countTags(); + final totalReminders = reminderModule.reminders.length; + final botStartDateTime = startDurationModule.startDate; + final docsUpdateDateTime = docsModule.lastUpdate; + + return BotInfo( + cachedGuilds: cachedGuilds, + cachedUsers: cachedUsers, + cachedChannels: cachedChannels, + cachedVoiceStates: cachedVoiceStates, + shardCount: shardCount, + cachedMessages: cachedMessages, + totalTagsCount: totalTags, + totalRemainderCount: totalReminders, + uptime: botStartDateTime, + docsUpdate: docsUpdateDateTime); + } +} diff --git a/lib/src/settings.dart b/lib/src/settings.dart index 5bae0b1..42cdd38 100644 --- a/lib/src/settings.dart +++ b/lib/src/settings.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:nyxx/nyxx.dart'; -String get version => '4.9.3'; +String get version => '4.10.0-dev.7'; /// Get a [String] from an environment variable, throwing an exception if it is not set. /// @@ -17,6 +17,11 @@ String getEnv(String key, [String? def]) => /// instead of throwing an exception. bool getEnvBool(String key, [bool? def]) => ['true', '1'].contains(getEnv(key, def?.toString()).toLowerCase()); +/// Get a [int] from an environment variable, throwing an exception if cannot be parsed to int. +int getEnvInt(String key, [int? def]) => + int.tryParse(getEnv(key, def.toString())) ?? + (throw Exception('Environment variable `$key` cannot be parsed as int')); + /// Name of the bot final String botName = getEnv('BOT_NAME', 'Running on Dart'); @@ -67,6 +72,22 @@ __Package repositories__: ${docsPackages.map((packageName) => '- $packageName: ').join('\n')} '''); +/// The custom content for web server alert box +final String webServerAlertContent = + getEnv('WEB_SERVER_ALERT_CONTENT', 'Experimental version'); + +/// Whether web server should be enabled +final bool webServerEnabled = getEnvBool('WEB_SERVER_ENABLE', false); + +/// The host of web server +final String webServerHost = getEnv("WEB_SERVER_HOST", 'localhost'); + +/// The port of web server +final int webServerPort = getEnvInt('WEB_SERVER_PORT', 8088); + +/// Path to templates directory +final String webServerTemplatesDirectory = getEnv('WEB_SERVER_TEMPLATES_DIRECTORY', "./templates"); + /// The GitHub account to use when no other account is specified. final String githubAccount = getEnv('ROD_GITHUB_ACCOUNT', 'nyxx-discord'); diff --git a/lib/src/web_app/api_server.dart b/lib/src/web_app/api_server.dart new file mode 100644 index 0000000..28eca24 --- /dev/null +++ b/lib/src/web_app/api_server.dart @@ -0,0 +1,152 @@ +import 'dart:convert'; + +import 'package:injector/injector.dart'; +import 'package:nyxx/nyxx.dart'; + +import 'package:running_on_dart/running_on_dart.dart'; +import 'package:running_on_dart/src/repository/feature_settings.dart'; +import 'package:running_on_dart/src/web_app/utils.dart'; +import 'package:running_on_dart/src/services/bot_info.dart'; +import 'package:shelf_cors_headers/shelf_cors_headers.dart'; + +import 'package:shelf_router/shelf_router.dart' as shelf_router; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; + +import 'package:http/http.dart' as http; +import 'package:shelf_session/cookies_middleware.dart'; +import 'package:shelf_session/session_middleware.dart'; + +final clientId = getEnv('DISCORD_CLIENT_ID'); +final clientSecret = getEnv('DISCORD_CLIENT_SECRET'); +final clientRedirectUri = getEnv('DISCORD_REDIRECT_URI'); + +class WebServer { + final Logger _logger = Logger('WebServer'); + + Future _handleGuilds(shelf.Request request) async { + if (!isAdminFromSession(request)) { + return shelf.Response.forbidden(null); + } + + final client = Injector.appInstance.get(); + + final guildData = Stream.fromIterable(client.guilds.cache.values).asyncMap((entry) async { + final guildChannels = client.channels.cache.values.whereType().where((c) => c.guildId == entry.id); + + final guildCachedMessages = guildChannels + .whereType() + .fold(0, (previous, channel) => previous + channel.messages.cache.length); + + final enabledFeatures = + await Injector.appInstance.get().fetchSettingsForGuild(entry.id); + + return { + 'id': entry.id.toString(), + 'name': entry.name, + 'banner': entry.bannerHash, + 'icon': entry.iconHash, + 'cached_members': entry.members.cache.length, + 'cached_channels': guildChannels.length, + 'cached_messages': guildCachedMessages, + 'cached_roles': entry.roles.cache.length, + 'enabled_features': enabledFeatures.map((s) => s.setting.name).join(", "), + }; + }); + + return createTwigResponse("guilds.html", parameters: { + 'guilds': await guildData.toList(), + }); + } + + Future _handleHx(shelf.Request request) async { + final templateName = request.url.queryParameters['c']; + if (templateName == null) { + return shelf.Response.badRequest(); + } + + final additionalParameters = switch (templateName) { + 'navigation' => { + 'clientId': clientId, + 'redirectUri': clientRedirectUri, + }, + 'alert' => { + 'inner_content': webServerAlertContent, + }, + _ => {}, + }; + + return createTwigResponse('component/$templateName.html', parameters: { + ...getCustomDataFromSession(request), + ...additionalParameters, + }); + } + + Future _handleIndex(shelf.Request request) async { + final data = await Injector.appInstance.get().getCurrentBotInfo(); + + return createTwigResponse("index.html", parameters: { + ...data.toJson(), + }); + } + + Future _handleRedirect(shelf.Request request) async { + final authCode = request.url.queryParameters['code']; + + final tokenResponse = await http.post(Uri.https('discord.com', '/api/oauth2/token'), body: { + 'client_id': clientId, + 'client_secret': clientSecret, + 'redirect_uri': clientRedirectUri, + 'grant_type': 'authorization_code', + 'code': authCode, + }, headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }); + + final tokenBodyJson = jsonDecode(tokenResponse.body); + final token = tokenBodyJson['access_token']; + + final userData = await http.get(Uri.https('discord.com', '/api/oauth2/@me'), headers: { + "Accept": "application/json", + "Authorization": "Bearer $token", + }); + final userDataJson = jsonDecode(userData.body); + + initSession(request, userDataJson); + + return shelf.Response.seeOther("/"); + } + + Future _handleLogOut(shelf.Request request) async { + deleteSession(request); + + return shelf.Response.seeOther("/"); + } + + Future _setupRouter() async { + return shelf_router.Router() + ..get("/", _sessionAware(_handleIndex)) + ..get("/guilds", _sessionAware(_handleGuilds)) + ..get("/hx", _sessionAware(_handleHx)) + ..get("/redirect", _sessionAware(_handleRedirect)) + ..get("/logout", _sessionAware(_handleLogOut)); + } + + shelf.Handler _sessionAware(shelf.Handler inner) => + shelf.Pipeline().addMiddleware(cookiesMiddleware()).addMiddleware(sessionMiddleware()).addHandler(inner); + + Future startServer() async { + if (!webServerEnabled) { + _logger.info("Web server not enabled skipping"); + return; + } + + final router = await _setupRouter(); + + final app = + const shelf.Pipeline().addMiddleware(shelf.logRequests()).addMiddleware(corsHeaders()).addHandler(router.call); + + _logger.info("Starting server at: http://$webServerHost:$webServerPort/"); + await shelf_io.serve(app, webServerHost, webServerPort); + } +} diff --git a/lib/src/web_app/session_manager_plugin.dart b/lib/src/web_app/session_manager_plugin.dart new file mode 100644 index 0000000..f2da484 --- /dev/null +++ b/lib/src/web_app/session_manager_plugin.dart @@ -0,0 +1,37 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:nyxx/nyxx.dart'; +import 'package:shelf_session/session_middleware.dart'; + +class SessionManagerPlugin extends NyxxPlugin { + static const _sessionsFile = '/sessions/sessions.json'; + + void _saveSessions() async { + await saveSessions((sessionData) async { + logger.info("Saving session file"); + await File(_sessionsFile).writeAsString(sessionData); + }); + } + + @override + FutureOr afterConnect(NyxxGateway client) async { + await restoreSessions(() async { + final file = File(_sessionsFile); + if (await file.exists()) { + logger.info("Loading session file."); + return File(_sessionsFile).readAsString(); + } + + logger.info("Session file missing. Returning default"); + return '{}'; + }); + + Timer.periodic(Duration(minutes: 15), (timer) => _saveSessions()); + } + + @override + FutureOr afterClose() async { + _saveSessions(); + } +} diff --git a/lib/src/web_app/utils.dart b/lib/src/web_app/utils.dart new file mode 100644 index 0000000..f992680 --- /dev/null +++ b/lib/src/web_app/utils.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:mustachex/mustachex.dart'; +import 'package:nyxx/nyxx.dart'; +import 'package:running_on_dart/running_on_dart.dart'; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf_session/session_middleware.dart'; + +shelf.Response createJsonErrorResponse(int errorCode, String errorMessage) { + return shelf.Response(errorCode, + body: jsonEncode({"message": errorMessage}), headers: {"Content-Type": 'application/json'}); +} + +shelf.Response createOkResponse(Object? body) { + return shelf.Response.ok(jsonEncode(body), headers: {"Content-Type": 'application/json'}); +} + +Future createTwigResponse(String name, {Map? parameters}) async { + final processor = MustachexProcessor(initialVariables: parameters); + final templateData = await File("$webServerTemplatesDirectory/$name").readAsString(); + + return shelf.Response.ok(await processor.process(templateData), headers: {"Content-Type": 'text/html'}); +} + +shelf.Response createUnauthorizedResponse(String errorMessage) => createJsonErrorResponse(400, errorMessage); + +shelf.Response createForbiddenResponse() => shelf.Response.forbidden(null); + +void initSession(shelf.Request request, Map userDataJson) { + var session = Session.getSession(request); + session ??= Session.createSession(request); + + final userId = userDataJson['user']['id'] as String; + + session.data['user_data'] = { + 'id': userId, + 'name': userDataJson['user']['global_name'] ?? userDataJson['user']['username'], + 'avatar': userDataJson['user']['avatar'], + 'expires_t': userDataJson['expires'], + }; + session.data['is_admin'] = adminIds.contains(Snowflake.parse(userId)); + + session.expires = DateTime.now().add(Duration(days: 3)); +} + +void deleteSession(shelf.Request request) { + Session.deleteSession(request); +} + +bool isAdminFromSession(shelf.Request request) { + final session = getSession(request); + + return session?.data['is_admin'] as bool? ?? false; +} + +Map getCustomDataFromSession(shelf.Request request) { + final session = getSession(request); + + return { + 'user_data': session?.data['user_data'] as Map? ?? false, + 'is_admin': session?.data['is_admin'] ?? false, + }; +} + +Session? getSession(shelf.Request request) { + final session = Session.getSession(request); + if (session == null) { + return null; + } + + final now = DateTime.now(); + if (session.data['user_data'] != null) { + final nowPlusOneDay = now.add(Duration(days: 1)); + + if (session.expires.isBefore(nowPlusOneDay)) { + session.expires = now.add(Duration(days: 1, hours: 12)); + } + } + + return session; +} diff --git a/pubspec.lock b/pubspec.lock index fbad2dd..85576a2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http_methods: + dependency: transitive + description: + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + url: "https://pub.dev" + source: hosted + version: "1.1.1" http_multi_server: dependency: transitive description: @@ -314,6 +322,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mustache_recase: + dependency: transitive + description: + name: mustache_recase + sha256: "63a339ebdc67305444e465519df27cbc5beb59fb69f7fe43c0f16c2b376a5d43" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + mustachex: + dependency: "direct main" + description: + name: mustachex + sha256: "6a9ea0861f3d8eb0db0862ee0dd3f1c6e4f623bb9fa47d13485c10f2fc946ca3" + url: "https://pub.dev" + source: hosted + version: "1.0.0" node_preamble: dependency: transitive description: @@ -398,10 +422,10 @@ packages: dependency: "direct main" description: name: postgres - sha256: e9802d4c9d78e432c4d4e57be9a91d99cf1552d45f17b17792f8e595060376a2 + sha256: d646bdc6f093babfa3337071c6f4ca2feabefdb7a6d18fc89f0f517c8d2e0819 url: "https://pub.dev" source: hosted - version: "3.4.3" + version: "3.4.4" pub_semver: dependency: transitive description: @@ -418,6 +442,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" retry: dependency: transitive description: @@ -451,13 +483,21 @@ packages: source: hosted version: "1.0.3" shelf: - dependency: transitive + dependency: "direct main" description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted version: "1.4.2" + shelf_cors_headers: + dependency: "direct main" + description: + name: shelf_cors_headers + sha256: a127c80f99bbef3474293db67a7608e3a0f1f0fcdb171dad77fa9bd2cd123ae4 + url: "https://pub.dev" + source: hosted + version: "0.1.5" shelf_packages_handler: dependency: transitive description: @@ -466,6 +506,23 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + shelf_router: + dependency: "direct main" + description: + name: shelf_router + sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 + url: "https://pub.dev" + source: hosted + version: "1.1.4" + shelf_session: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "69aadbbd7572afcd076e122ae2ba63f1839a710e" + url: "https://github.com/l7ssha/shelf_session.git" + source: git + version: "0.1.2" shelf_static: dependency: transitive description: @@ -478,10 +535,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" source_map_stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2dd3621..f65d4e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: running_on_dart -version: 4.9.3 +version: 4.10.0-dev.7 description: Discord Bot for nyxx development homepage: https://github.com/nyxx-discord/running_on_dart repository: https://github.com/nyxx-discord/running_on_dart @@ -27,9 +27,18 @@ dependencies: git: https://github.com/l7ssha/Tentacle.git injector: ^4.0.0 + # directly used transitive dependencies dio: any built_collection: any + # api server + shelf: ^1.4.2 + shelf_router: ^1.1.4 + shelf_cors_headers: ^0.1.0 + shelf_session: + git: https://github.com/l7ssha/shelf_session.git + mustachex: ^1.0.0 + dev_dependencies: lints: ^5.0.0 test: ^1.25.8 diff --git a/templates/component/alert.html b/templates/component/alert.html new file mode 100644 index 0000000..9cbd30e --- /dev/null +++ b/templates/component/alert.html @@ -0,0 +1,3 @@ + diff --git a/templates/component/navigation.html b/templates/component/navigation.html new file mode 100644 index 0000000..2ab9a4f --- /dev/null +++ b/templates/component/navigation.html @@ -0,0 +1,40 @@ + diff --git a/templates/guilds.html b/templates/guilds.html new file mode 100644 index 0000000..86af46b --- /dev/null +++ b/templates/guilds.html @@ -0,0 +1,57 @@ + + + + + Running on Dart + + + + + + + +
+
+ +
+
+ {{#guilds}} +
+

+ +

+
+
+ + + + + + + + + + + + + + + + + +
Cached membersCached channelsCached messagesCached roles
{{cached_members}}{{cached_channels}}{{cached_messages}}{{cached_roles}}
+

+ Features: {{enabled_features}} +

+
+
+
+ {{/guilds}} +
+
+
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a53248b --- /dev/null +++ b/templates/index.html @@ -0,0 +1,133 @@ + + + + + Running on Dart + + + + + + + + + + +
+
+ +
+
+

Cache Info

+
+
+
+
+
{{cached_channels}}
+
Cached channels
+
+
+ +
+
+
{{cached_messages}}
+
Cached messages
+
+
+ +
+
+
{{cached_guilds}}
+
Cached guilds
+
+
+ +
+
+
{{cached_users}}
+
Cached Users
+
+
+ +
+
+
{{cached_voice_states}}
+
Cached voice states
+
+
+
+
+
+
+

Modules Info

+
+
+
+
+
{{total_tags_count}}
+
Total tags
+
+
+ +
+
+
{{total_reminder_count}}
+
Total reminders
+
+
+ +
+
+ +
n/a
+
Last docs update
+
+
+
+
+
+
+

Program Info

+
+
+
+
+
{{nyxx_version}}
+
Nyxx Version
+
+
+ +
+
+
{{version}}
+
Bot Version
+
+
+ +
+
+
{{platform}}
+
Dart Version
+
+
+ +
+
+
{{memory_usage_string}}
+
Memory usage
+
+
+
+
+
+
+ +
n/a
+
Uptime
+
+
+
+
+
+ +