Skip to content

Refactor to module based pattern#8

Merged
oelburk merged 9 commits intomasterfrom
refactor-bot-modules
Jan 24, 2025
Merged

Refactor to module based pattern#8
oelburk merged 9 commits intomasterfrom
refactor-bot-modules

Conversation

@oelburk
Copy link
Owner

@oelburk oelburk commented Jul 25, 2023

No description provided.

@QodoAI-Agent
Copy link

PR Analysis

  • 🎯 Main theme: Refactoring of the bot commands into a module based pattern
  • 📌 Type of PR: Refactoring
  • 🧪 Relevant tests added: No
  • Focused PR: Yes, the PR is focused as it is primarily concerned with refactoring the bot commands into a module based pattern. The title and description (although missing) align with the changes made in the PR.
  • 🔒 Security concerns: No, the PR does not introduce any apparent security concerns. It mainly involves refactoring and does not seem to introduce any features that would pose a security risk.

PR Feedback

  • 💡 General PR suggestions: The PR shows good effort in refactoring the code into a more modular structure, which is a good practice. However, there are some areas that could be improved. Firstly, it's important to include a description in your PR to give reviewers a high-level understanding of the changes. Secondly, consider adding tests to ensure that your changes work as expected and prevent future regressions. Lastly, make sure to handle exceptions properly in your code to avoid potential crashes or unexpected behavior.

How to use

Tag me in a comment '@CodiumAI-Agent' and add one of the following commands:
/review [-i]: Request a review of your Pull Request. For an incremental review, which only considers changes since the last review, include the '-i' option.
/describe [-c]: Modify the PR title and description based on the contents of the PR. To get the description as comment instead of modifying the PR description, include the '-c' option.
/improve: Suggest improvements to the code in the PR. These will be provided as pull request comments, ready to commit.
/ask <QUESTION>: Pose a question about the PR.

@QodoAI-Agent
Copy link

PR Title

Refactor bot modules

PR Type

Refactoring

PR Description

This PR is a major refactor of the bot's codebase. It introduces a new structure for the bot's commands and functionalities, grouping them into separate modules. The main changes include the creation of a new 'UntappdModule' for handling Untappd-related commands and functionalities, and the refactoring of existing commands into this new structure. The PR also includes changes to the bot's dependencies and the removal of some unused files.

PR Main Files Walkthrough

-bin/commands.dart: The main command handling file has been significantly refactored. Most of the command handling code has been moved to the new 'UntappdModule'. The file now mainly imports the new module and initializes the commands.
-bin/modules/untappd/untapped_module.dart: This is a new file that contains the main logic for the Untappd-related functionalities of the bot. It includes the initialization of the module, the handling of Untappd-related commands, and the fetching and updating of Untappd checkins.
-bin/utils.dart: Minor changes to remove unused imports.
-bin/modules/untappd/commands.dart: This new file contains the command handling code for the Untappd-related commands.
-bin/modules/beer_agent/models/beer.dart: Added a new 'BeerList' class for handling lists of beers.
-bin/modules/untappd/models/untappd_checkin.dart: This new file defines the 'UntappdCheckin' class, which represents a checkin on Untappd.
-bin/modules/bot_module.dart: This new file defines the 'BotModule' abstract class, which is the base class for all bot modules.
-bin/modules/untappd/hive_constants.dart: This new file contains constants related to the Hive local data storage used by the Untappd module.

Repository owner deleted a comment from QodoAI-Agent Jul 30, 2023
@oelburk
Copy link
Owner Author

oelburk commented Jul 30, 2023

@CodiumAI-Agent /improve

Comment on lines +16 to +26
...UntappdModule().commands,
];

static Future<void> _helpCommand(ISlashCommandInteractionEvent ctx) async {
var helpMessage = MessageBuilder()
..append(ctx.interaction.userAuthor!.mention)
..appendNewLine()
..append('Did anyone say beer? This is what I can do for you:')
..appendNewLine()
..appendNewLine()
..appendBold('/oel')
..appendNewLine()
..append('Lists all known beer releases.')
..appendNewLine()
..appendNewLine()
..appendBold('/subscribe')
..appendNewLine()
..append(
'Subscribe to automatic beer release reminders. Reminders will be posted 3 times during the day before release.')
..appendNewLine()
..appendNewLine()
..appendBold('/release YYYY-MM-dd')
..appendNewLine()
..append(
'Posts the beer release for given date in the format YYYY-MM-dd. e.g ')
..appendItalics('/release 1970-01-30')
..appendNewLine()
..appendNewLine()
..appendBold('/help')
..append(_mainHelpMessage)
..appendNewLine()
..append('Shows you this help message.')
..appendNewLine()
..appendNewLine()
..appendBold('/untappd untappd_username')
..appendNewLine()
..append(
'Let me know your untappd username so I can post automatic updates from your untappd account. e.g ')
..appendItalics('/untappd cornholio')
..appendNewLine()
..appendNewLine()
..appendBold('/setup')
..appendNewLine()
..append(
'Setup the bot to post untappd updates to the current channel. Only admins can issue this command. Also, this is needed before any untappd updates can occur.');
..append(UntappdModule().helpMessage);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Avoid creating multiple instances of UntappdModule in the code. Instead, create a single instance and reuse it. This will improve performance and memory usage.

Suggested change
...UntappdModule().commands,
];
static Future<void> _helpCommand(ISlashCommandInteractionEvent ctx) async {
var helpMessage = MessageBuilder()
..append(ctx.interaction.userAuthor!.mention)
..appendNewLine()
..append('Did anyone say beer? This is what I can do for you:')
..appendNewLine()
..appendNewLine()
..appendBold('/oel')
..appendNewLine()
..append('Lists all known beer releases.')
..appendNewLine()
..appendNewLine()
..appendBold('/subscribe')
..appendNewLine()
..append(
'Subscribe to automatic beer release reminders. Reminders will be posted 3 times during the day before release.')
..appendNewLine()
..appendNewLine()
..appendBold('/release YYYY-MM-dd')
..appendNewLine()
..append(
'Posts the beer release for given date in the format YYYY-MM-dd. e.g ')
..appendItalics('/release 1970-01-30')
..appendNewLine()
..appendNewLine()
..appendBold('/help')
..append(_mainHelpMessage)
..appendNewLine()
..append('Shows you this help message.')
..appendNewLine()
..appendNewLine()
..appendBold('/untappd untappd_username')
..appendNewLine()
..append(
'Let me know your untappd username so I can post automatic updates from your untappd account. e.g ')
..appendItalics('/untappd cornholio')
..appendNewLine()
..appendNewLine()
..appendBold('/setup')
..appendNewLine()
..append(
'Setup the bot to post untappd updates to the current channel. Only admins can issue this command. Also, this is needed before any untappd updates can occur.');
..append(UntappdModule().helpMessage);
var untappdModule = UntappdModule();
...
untappdModule.commands,
...
untappdModule.helpMessage

Comment on lines +201 to +214
@override
void init(INyxxWebsocket bot,
{Duration updateInterval = const Duration(minutes: 12)}) {
// Set up Hive for local data storage
Hive.init('/data');
Hive.openBox(HiveConstants.untappdBox);

_bot = bot;

// Start timer to check for untappd updates
Timer.periodic(updateInterval, (timer) => _checkUntappd());

// Set module as initialized
_isInitialized = true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Use dependency injection to provide the INyxxWebsocket bot to the UntappdModule instead of setting it in the init method. This will make the code more testable and modular.

Suggested change
@override
void init(INyxxWebsocket bot,
{Duration updateInterval = const Duration(minutes: 12)}) {
// Set up Hive for local data storage
Hive.init('/data');
Hive.openBox(HiveConstants.untappdBox);
_bot = bot;
// Start timer to check for untappd updates
Timer.periodic(updateInterval, (timer) => _checkUntappd());
// Set module as initialized
_isInitialized = true;
UntappdModule(INyxxWebsocket bot) {
...
_bot = bot;
...
}

Comment on lines +94 to +96
} catch (e) {
print(e.toString());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider handling exceptions in a more user-friendly way. Instead of just printing the error, you could log it and show a user-friendly message to the user.

Suggested change
} catch (e) {
print(e.toString());
}
catch (e) {
Logger().e('Error while checking Untappd: $e');
throw UserFriendlyException('An error occurred while checking Untappd.');
}

Comment on lines +27 to +98
void _checkUntappd() async {
if (!_isInitialized) {
print('Untappd module not initialized!');
throw Exception('Untappd module not initialized!');
}
var box = Hive.box(HiveConstants.untappdBox);

Map<dynamic, dynamic> listOfUsers =
await box.get(HiveConstants.untappdUserList, defaultValue: {});
var latestCheckins = await box
.get(HiveConstants.untappdLatestUserCheckins, defaultValue: {});

var updateChannelId = await box.get(HiveConstants.untappdUpdateChannelId);

if (updateChannelId == null) {
print('No channel available for updates!');
return;
}

if (listOfUsers.isEmpty) print('No users available to scrape!');

listOfUsers.forEach((userSnowflake, untappdUsername) async {
var latestCheckinDisk = latestCheckins[untappdUsername];
try {
var latestCheckinUntappd = await _getLatestCheckin(untappdUsername);

// If a new ID is available, post update!
if (latestCheckinUntappd != null &&
latestCheckinDisk != latestCheckinUntappd.id) {
// Update latest saved checkin
latestCheckins.addAll({untappdUsername: latestCheckinUntappd.id});
await box.put(
HiveConstants.untappdLatestUserCheckins, latestCheckins);

// Build update message with info from untappd checkin
var user = await _bot.fetchUser(Snowflake(userSnowflake));
var embedBuilder = EmbedBuilder();
embedBuilder.title = '${user.username} is drinking beer!';
embedBuilder.url =
_getCheckinUrl(latestCheckinUntappd.id, untappdUsername);
embedBuilder.description = latestCheckinUntappd.title;
if (latestCheckinUntappd.comment.isNotEmpty) {
embedBuilder.addField(
field:
EmbedFieldBuilder('Comment', latestCheckinUntappd.comment));
}
if (latestCheckinUntappd.rating.isNotEmpty) {
embedBuilder.addField(
field: EmbedFieldBuilder(
'Rating',
_buildRatingEmoji(
double.parse(latestCheckinUntappd.rating))));
}
if (latestCheckinUntappd.photoAddress != null) {
embedBuilder.imageUrl = latestCheckinUntappd.photoAddress;
}

// Get channel used for untappd updates, previously set by discord admin.
var updateChannel = await _bot
.fetchChannel(Snowflake(updateChannelId))
.then((value) => (value as ITextChannel));

// Send update message
await updateChannel.sendMessage(MessageBuilder.embed(embedBuilder));
}
// Sleep 5 seconds per user to avoid suspicious requests to untappd server
await Future.delayed(Duration(seconds: 5));
} catch (e) {
print(e.toString());
}
});
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider using a more descriptive name for the _checkUntappd method. The current name does not clearly indicate what the method does.

Suggested change
void _checkUntappd() async {
if (!_isInitialized) {
print('Untappd module not initialized!');
throw Exception('Untappd module not initialized!');
}
var box = Hive.box(HiveConstants.untappdBox);
Map<dynamic, dynamic> listOfUsers =
await box.get(HiveConstants.untappdUserList, defaultValue: {});
var latestCheckins = await box
.get(HiveConstants.untappdLatestUserCheckins, defaultValue: {});
var updateChannelId = await box.get(HiveConstants.untappdUpdateChannelId);
if (updateChannelId == null) {
print('No channel available for updates!');
return;
}
if (listOfUsers.isEmpty) print('No users available to scrape!');
listOfUsers.forEach((userSnowflake, untappdUsername) async {
var latestCheckinDisk = latestCheckins[untappdUsername];
try {
var latestCheckinUntappd = await _getLatestCheckin(untappdUsername);
// If a new ID is available, post update!
if (latestCheckinUntappd != null &&
latestCheckinDisk != latestCheckinUntappd.id) {
// Update latest saved checkin
latestCheckins.addAll({untappdUsername: latestCheckinUntappd.id});
await box.put(
HiveConstants.untappdLatestUserCheckins, latestCheckins);
// Build update message with info from untappd checkin
var user = await _bot.fetchUser(Snowflake(userSnowflake));
var embedBuilder = EmbedBuilder();
embedBuilder.title = '${user.username} is drinking beer!';
embedBuilder.url =
_getCheckinUrl(latestCheckinUntappd.id, untappdUsername);
embedBuilder.description = latestCheckinUntappd.title;
if (latestCheckinUntappd.comment.isNotEmpty) {
embedBuilder.addField(
field:
EmbedFieldBuilder('Comment', latestCheckinUntappd.comment));
}
if (latestCheckinUntappd.rating.isNotEmpty) {
embedBuilder.addField(
field: EmbedFieldBuilder(
'Rating',
_buildRatingEmoji(
double.parse(latestCheckinUntappd.rating))));
}
if (latestCheckinUntappd.photoAddress != null) {
embedBuilder.imageUrl = latestCheckinUntappd.photoAddress;
}
// Get channel used for untappd updates, previously set by discord admin.
var updateChannel = await _bot
.fetchChannel(Snowflake(updateChannelId))
.then((value) => (value as ITextChannel));
// Send update message
await updateChannel.sendMessage(MessageBuilder.embed(embedBuilder));
}
// Sleep 5 seconds per user to avoid suspicious requests to untappd server
await Future.delayed(Duration(seconds: 5));
} catch (e) {
print(e.toString());
}
});
}
void _fetchAndPostUntappdUpdates() async {
...
}

Copy link

@Hampusheglert Hampusheglert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job!
Slightly to big PR to actually give good feedback, but got some small inputs 👯


late INyxxWebsocket _bot;

static final BeerAgentModule _singleton = BeerAgentModule._internal();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use injectable package for singleton and to provide the INyxxWebsocket bot.

Future<void> _updateSubscribers() async {
await _updateBeerSales();

var myFile = File('sub.dat');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this path be moved to a constants file?

..appendNewLine()
..append('There is a fresh beer release tomorrow, ')
..appendBold(DateFormat('yyyy-MM-dd').format(saleDate))
..append('. Bolaget opens 10:00')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be cool if this actually fetches the openings hours instead of hardcoding, in case of the opening hours actually changing to 9 and all the people in this discord misses the beer and it's already sold out when they arrive.

}

/// Fetches a list of all beer sales from online API.
Future<Map<String, dynamic>> _fetchBeerList() async {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use final instead? :D


/// Returns a list of all channels (users) that are subscribed to beer updates.
Future<List<IDMChannel>> _getSubChannels(INyxxWebsocket bot) async {
var myFile = File('sub.dat');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this could be saved in a constant file to avoid bugs in the future.

Future<void> _releaseCommand(ISlashCommandInteractionEvent ctx) async {
var input = ctx.args;
if (input.length == 1) {
var parsedDate = DateTime.tryParse(input[0].value);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could use "input.first.value"
it will throw stateerror if empty.

..append(ctx.interaction.userAuthor!.mention)
..appendNewLine()
..append(' :beers: ')
..appendBold(input[0].value)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input.first.value

Comment on lines +50 to +66
class BeerList {
final String saleDate;
final List<Beer> beerList;

BeerList(this.saleDate, this.beerList);
BeerList.fromJson(Map<String, dynamic> json)
: saleDate = json['first_sale'],
beerList = createListFromMap(json['items']);

static List<Beer> createListFromMap(List<dynamic> json) {
var toReturn = <Beer>[];
for (var item in json) {
toReturn.add(Beer.fromJson(item));
}
return toReturn;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

freezed package could clean up this a bit

Comment on lines +101 to +107
String _buildRatingEmoji(double rating) {
var ratingString = '';
for (var i = 0; i < rating.toInt(); i++) {
ratingString += ':beer: ';
}
return '$ratingString ($rating)';
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final ratingString = List.filled(rating.toInt(),':beer: ', growable: false).join();

throw 'No checkins are available for $untappdUsername';
}

var latestCheckin = checkins.first!;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bang bang, you shot me down... bang bang

@oelburk oelburk merged commit a854108 into master Jan 24, 2025
1 check passed
@QodoAI-Agent
Copy link

PR Code Suggestions ✨

CategorySuggestion                                                                                                                                    Score
Possible issue
Add error handling for API response parsing

Add error handling for the _fetchBeerList method to ensure that the application does
not crash if the API response is malformed or unavailable. Consider logging the
error and providing a fallback mechanism.

bin/modules/beer_agent/beer_agent_module.dart [95-103]

 final response = await http.get(Uri.parse(
     'https://systembevakningsagenten.se/api/json/2.0/newProducts.json'));
 
 if (response.statusCode == 200) {
-  Map<String, dynamic> res = json.decode(response.body);
-  return res;
+  try {
+    Map<String, dynamic> res = json.decode(response.body);
+    return res;
+  } catch (e) {
+    print('Error parsing beer list: $e');
+    return {};
+  }
 } else {
-  throw Exception('Error fetching beer information');
+  print('Error fetching beer information: ${response.statusCode}');
+  return {};
 }
Suggestion importance[1-10]: 9

Why: The suggestion adds robust error handling for the _fetchBeerList method, ensuring the application does not crash due to malformed API responses. This is a critical improvement for reliability and user experience.

9
Add timeout to prevent indefinite hangs

Add a timeout mechanism to the _checkUntappd method to prevent the function from
hanging indefinitely if an external service like Untappd or Discord API becomes
unresponsive.

bin/modules/untappd/untapped_module.dart [48-52]

 listOfUsers.forEach((userSnowflake, untappdUsername) async {
   var latestCheckinDisk = latestCheckins[untappdUsername];
   try {
-    var latestCheckinUntappd = await _getLatestCheckin(untappdUsername);
+    var latestCheckinUntappd = await _getLatestCheckin(untappdUsername).timeout(Duration(seconds: 10));
     ...
Suggestion importance[1-10]: 9

Why: Adding a timeout to the _getLatestCheckin call ensures that the function does not hang indefinitely if the external service is unresponsive. This is a critical improvement for reliability and robustness.

9
Ensure Hive box opens successfully

Add a check to ensure that the Hive box is successfully opened in the init method
before proceeding with further operations to avoid runtime errors.

bin/modules/untappd/untapped_module.dart [205-206]

 Hive.init('/data');
-Hive.openBox(HiveConstants.untappdBox);
+var box = await Hive.openBox(HiveConstants.untappdBox);
+if (box == null) {
+  throw Exception('Failed to open Hive box');
+}
 _bot = bot;
Suggestion importance[1-10]: 9

Why: Adding a check to confirm that the Hive box opens successfully prevents runtime errors and ensures the application can handle initialization failures gracefully. This is a critical improvement for stability.

9
Handle invalid sale date formats

Ensure that the _updateSubscribers method handles cases where the saleDate is null
or improperly formatted to prevent runtime errors.

bin/modules/beer_agent/beer_agent_module.dart [45-49]

-saleDate = DateTime.parse(sale.saleDate);
-var currentDate = DateTime(
-    DateTime.now().year, DateTime.now().month, DateTime.now().day);
-if (saleDate.difference(currentDate).inDays == 1) {
+try {
+  saleDate = DateTime.parse(sale.saleDate);
+  var currentDate = DateTime(
+      DateTime.now().year, DateTime.now().month, DateTime.now().day);
+  if (saleDate.difference(currentDate).inDays == 1) {
+} catch (e) {
+  print('Invalid sale date format: ${sale.saleDate}');
+  continue;
+}
Suggestion importance[1-10]: 8

Why: The suggestion ensures that invalid or improperly formatted saleDate values are handled gracefully, preventing runtime errors. This is a significant improvement for stability and error resilience.

8
Handle invalid channel IDs in subscription file

Add a check in _getSubChannels to handle cases where the sub.dat file contains
invalid or non-existent channel IDs to prevent runtime errors.

bin/modules/beer_agent/beer_agent_module.dart [116-118]

-var chan = await bot
-    .fetchChannel(Snowflake(line))
-    .then((value) => (value as IDMChannel));
+try {
+  var chan = await bot
+      .fetchChannel(Snowflake(line))
+      .then((value) => (value as IDMChannel));
+  channelList.add(chan);
+} catch (e) {
+  print('Invalid or non-existent channel ID: $line');
+}
 
-channelList.add(chan);
-
Suggestion importance[1-10]: 8

Why: Adding error handling for invalid or non-existent channel IDs in the subscription file prevents runtime crashes and improves the robustness of the _getSubChannels method.

8
Validate rating value range

Validate the rating value in _buildRatingEmoji to ensure it is within an expected
range (e.g., 0-5) to avoid unexpected behavior or errors.

bin/modules/untappd/untapped_module.dart [101-107]

 String _buildRatingEmoji(double rating) {
+  if (rating < 0 || rating > 5) {
+    throw ArgumentError('Rating must be between 0 and 5');
+  }
   var ratingString = '';
   for (var i = 0; i < rating.toInt(); i++) {
     ratingString += ':beer: ';
   }
   return '$ratingString ($rating)';
 }
Suggestion importance[1-10]: 7

Why: Validating the rating value ensures that the _buildRatingEmoji method behaves as expected and avoids potential errors or unexpected behavior. This is a good practice for input validation.

7
General
Improve exception handling granularity

Ensure that the _checkUntappd method handles exceptions more granularly,
distinguishing between network errors, parsing errors, and other unexpected issues
for better debugging and reliability.

bin/modules/untappd/untapped_module.dart [94-96]

 } catch (e) {
-  print(e.toString());
+  if (e is TimeoutException) {
+    print('Request timed out: ${e.message}');
+  } else if (e is FormatException) {
+    print('Data format error: ${e.message}');
+  } else {
+    print('Unexpected error: ${e.toString()}');
+  }
 }
Suggestion importance[1-10]: 8

Why: Enhancing exception handling to distinguish between different error types improves debugging and reliability. This is a valuable improvement for better error diagnostics and handling.

8
Validate date input in release command

Add a check in _releaseCommand to ensure that the parsedDate is not null before
proceeding with further operations to avoid potential null pointer exceptions.

bin/modules/beer_agent/commands.dart [127-129]

-if (parsedDate != null) {
-  await BeerAgentModule()._updateBeerSales();
-  for (var sale in BeerAgentModule().beerSales) {
+if (parsedDate == null) {
+  await ctx.respond(MessageBuilder.content(
+      ctx.interaction.userAuthor!.mention +
+      ' Invalid date format. Please use YYYY-MM-dd.'));
+  return;
+}
+await BeerAgentModule()._updateBeerSales();
+for (var sale in BeerAgentModule().beerSales) {
Suggestion importance[1-10]: 7

Why: The suggestion ensures that the parsedDate is validated before proceeding, avoiding potential null pointer exceptions. This is a useful improvement for input validation and error handling.

7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants