Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: web server #40

Draft
wants to merge 42 commits into
base: rewrite
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c856f02
Initial api server commit
l7ssha Oct 31, 2024
ae2ac60
Initial jwt auth implementation
l7ssha Oct 31, 2024
7a35eab
Fix jwt parsing
l7ssha Oct 31, 2024
39c14f4
Minor naming fixes
l7ssha Oct 31, 2024
acf46d1
Implement oauth discord login
l7ssha Oct 31, 2024
e6aca21
pubspec.yaml missing new line at the end of file
l7ssha Oct 31, 2024
141f36e
Improvements for generating jwt token
l7ssha Nov 1, 2024
02f647d
Bot info collection improvements
l7ssha Nov 1, 2024
a649cca
Fix missing public bot info fields
l7ssha Nov 2, 2024
0a887fa
Improve handling of endpoint permissions
l7ssha Nov 2, 2024
5044d97
Send user data with token
l7ssha Nov 2, 2024
84f5365
Extend guild data
l7ssha Nov 2, 2024
201d2a4
Initial commit
l7ssha Nov 18, 2024
caa5b37
Merge branch 'rewrite' into feature/web-server-v2
l7ssha Nov 18, 2024
843e30e
Implement session support
l7ssha Nov 18, 2024
cb6b076
Refactor session handling; Implement main page statistics
l7ssha Nov 18, 2024
a83c5d7
Merge branch 'rewrite' into feature/web-server-v2
l7ssha Nov 19, 2024
f0dbaa5
Display logged-in user avatar (doesn't really work now)
l7ssha Nov 19, 2024
4f2a05f
Merge branch 'rewrite' into feature/web-server-v2
l7ssha Nov 20, 2024
3c60c66
Rework frontend to bootstrap
l7ssha Nov 20, 2024
d2856f7
Implement htmx; Implement guilds; Save sessions to file
l7ssha Nov 21, 2024
6eebd16
Pass only needed parameters for template
l7ssha Nov 21, 2024
bc48e76
Add web server configuration
l7ssha Nov 21, 2024
9ce2f44
Prepare infra for web server
l7ssha Nov 21, 2024
49c7fd7
Release 4.10.0-dev.0
l7ssha Nov 21, 2024
63491d2
dart format
l7ssha Nov 21, 2024
862cb34
Fix getEnvInt default value
l7ssha Nov 21, 2024
ce65279
Remove image if cannot load
l7ssha Nov 21, 2024
0f0edb0
Release 4.10.0-dev.1
l7ssha Nov 21, 2024
9774eb6
Save sessions periodically instead of restoring them
l7ssha Nov 21, 2024
15fc485
Release 4.10.0-dev.2
l7ssha Nov 21, 2024
30113f2
Move timer to correct place
l7ssha Nov 21, 2024
9a12c00
Merge branch 'rewrite' into feature/web-server-v2
l7ssha Nov 22, 2024
dff8520
Extend session after logging in; Extend session initial duration to 3…
l7ssha Nov 22, 2024
deb76ad
Release 4.10.0-dev.3
l7ssha Nov 22, 2024
c7cc55c
Release 4.10.0-dev.4
l7ssha Nov 22, 2024
dca2dca
Release 4.10.0-dev.5
l7ssha Nov 22, 2024
84405d4
Rework guilds template
l7ssha Nov 23, 2024
5de317f
Release 4.10.0-dev.6
l7ssha Nov 23, 2024
1f01609
Fix index page on mobile
l7ssha Nov 23, 2024
5c41ce7
Merge branch 'rewrite' into feature/web-server-v2
l7ssha Nov 23, 2024
bedcc25
Release 4.10.0-dev.7
l7ssha Nov 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ doc/api/
*.db

lib_old/
sessions/
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions bin/running_on_dart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ void main() async {
IgnoreExceptions(),
commands,
pagination,
SessionManagerPlugin(),
],
));

await setupContainer(client);

WebServer().startServer();
}
18 changes: 15 additions & 3 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
7 changes: 5 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/running_on_dart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
64 changes: 18 additions & 46 deletions lib/src/commands/info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<BotInfoService>().getCurrentBotInfo();

final startDate = Injector.appInstance.get<BotStartDuration>().startDate;
final startDateStr =
"${startDate.format(TimestampStyle.longDateTime)} (${startDate.format(TimestampStyle.relativeTime)})";

final docsUpdatedDate = Injector.appInstance.get<DocsModule>().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<num>(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<TextChannel>()
.map((c) => c.messages.cache.length)
.fold<num>(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<TagModule>().countCachedTags(context.guild?.id ?? context.user.id).toString(),
isInline: true),
EmbedFieldBuilder(
name: 'Current reminders',
value: Injector.appInstance.get<ReminderModule>().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),
],
Expand Down
4 changes: 3 additions & 1 deletion lib/src/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -39,7 +40,8 @@ Future<void> setupContainer(NyxxGateway client) async {
..registerSingleton(() => JellyfinModuleV2())
..registerSingleton(() => MentionsMonitoringModule())
..registerSingleton(() => KavitaModule())
..registerSingleton(() => EmojiReactModule());
..registerSingleton(() => EmojiReactModule())
..registerSingleton(() => BotInfoService());

await Injector.appInstance.get<DatabaseService>().init();
await Injector.appInstance.get<FeatureSettingsModule>().init();
Expand Down
1 change: 1 addition & 0 deletions lib/src/modules/tag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tag> getGuildTags(Snowflake guildId) => tags.where((tag) => tag.guildId == guildId && tag.enabled);
Expand Down
96 changes: 96 additions & 0 deletions lib/src/services/bot_info.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> 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<BotInfo> 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<num>(0, (value, element) => value + element)
.ceil();
final shardCount = client.gateway.shards.length;
final cachedMessages = client.channels.cache.values
.whereType<TextChannel>()
.map((c) => c.messages.cache.length)
.fold<num>(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);
}
}
23 changes: 22 additions & 1 deletion lib/src/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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');

Expand Down Expand Up @@ -67,6 +72,22 @@ __Package repositories__:
${docsPackages.map((packageName) => '- $packageName: <https://github.com/nyxx-discord/$packageName>').join('\n')}
''');

/// The custom content for web server alert box
final String webServerAlertContent =
getEnv('WEB_SERVER_ALERT_CONTENT', '<span class="bold">Experimental version</span>');

/// 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');

Expand Down
Loading
Loading