diff --git a/.checkstyle/checkstyle-suppressions.xml b/.checkstyle/checkstyle-suppressions.xml new file mode 100644 index 0000000..13997f0 --- /dev/null +++ b/.checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/.checkstyle/checkstyle.xml b/.checkstyle/checkstyle.xml new file mode 100644 index 0000000..dc19a49 --- /dev/null +++ b/.checkstyle/checkstyle.xml @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fd553dc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,1014 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 130 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_wrap_on_typing = true + +[*.conf] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_hocon_keep_blank_lines_before_right_brace = 2 +ij_hocon_keep_indents_on_empty_lines = false +ij_hocon_keep_line_breaks = true +ij_hocon_space_after_colon = true +ij_hocon_space_after_comma = true +ij_hocon_space_before_colon = true +ij_hocon_space_before_comma = false +ij_hocon_spaces_within_braces = false +ij_hocon_spaces_within_brackets = false +ij_hocon_spaces_within_method_call_parentheses = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 1 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_at_first_column = true +ij_java_call_parameters_new_line_after_left_paren = true +ij_java_call_parameters_right_paren_on_new_line = true +ij_java_call_parameters_wrap = on_every_item +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 100000 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = true +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = false +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_enum_constants_wrap = split_into_lines +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = normal +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = true +ij_java_generate_final_parameters = true +ij_java_if_brace_force = always +ij_java_imports_layout = *,|,$* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 0 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_at_first_column = true +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = on_every_item +ij_java_method_parameters_new_line_after_left_paren = true +ij_java_method_parameters_right_paren_on_new_line = true +ij_java_method_parameters_wrap = on_every_item +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 100000 +ij_java_new_line_after_lparen_in_record_header = false +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = false +ij_java_prefer_parameters_wrap = true +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_record_header = false +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = true +ij_java_ternary_operation_wrap = on_every_item +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = normal +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = true +ij_java_wrap_long_lines = false + +[*.nbtt] +max_line_length = 150 +ij_continuation_indent_size = 4 +ij_nbtt_keep_indents_on_empty_lines = false +ij_nbtt_space_after_colon = true +ij_nbtt_space_after_comma = true +ij_nbtt_space_before_colon = true +ij_nbtt_space_before_comma = false +ij_nbtt_spaces_within_brackets = false +ij_nbtt_spaces_within_parentheses = false + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant, *.fxml, *.jhm, *.jnlp, *.jrxml, *.pom, *.rng, *.tld, *.wsdl, *.xml, *.xsd, *.xsl, *.xslt, *.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats, *.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = global +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash, *.sh, *.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false + +[{*.cjs, *.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = global +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.ft, *.vm, *.vsl}] +ij_vtl_keep_indents_on_empty_lines = false + +[{*.gant, *.gradle, *.groovy, *.gy}] +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_list_or_map = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = true +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_class_count_to_use_import_on_demand = 5 +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_if_brace_force = never +ij_groovy_import_annotation_wrap = 2 +ij_groovy_imports_layout = *, |, javax.**, java.**, |, $* +ij_groovy_indent_case_from_switch = true +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_names_count_to_use_import_on_demand = 3 +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_assert_separator = true +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = true +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_long_lines = false + +[{*.gradle.kts, *.kt, *.kts, *.main.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = off +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_right_paren_on_new_line = false +ij_kotlin_call_parameters_wrap = off +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = true +ij_kotlin_continuation_indent_for_expression_bodies = true +ij_kotlin_continuation_indent_in_argument_lists = true +ij_kotlin_continuation_indent_in_elvis = true +ij_kotlin_continuation_indent_in_if_conditions = true +ij_kotlin_continuation_indent_in_parameter_lists = true +ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = off +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = false +ij_kotlin_import_nested_classes = false +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = off +ij_kotlin_method_parameters_new_line_after_left_paren = false +ij_kotlin_method_parameters_right_paren_on_new_line = false +ij_kotlin_method_parameters_wrap = off +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_first_method_in_call_chain = false +ktlint_standard_filename = disabled + +[{*.har, *.jsb2, *.jsb3, *.json, .babelrc, .eslintrc, .stylelintrc, bowerrc, jest.config, mcmod.info}] +indent_size = 2 +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm, *.html, *.sht, *.shtm, *.shtml}] +ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p +ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span, pre, textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal +ij_html_uniform_ident = false + +[{*.yaml, *.yml}] +indent_size = 2 +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_space_before_colon = true +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..022b841 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c6cb0c8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: Citymonstret +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: [ 'https://paypal.me/Sauilitired' ]# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e27ac67 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,49 @@ +name: Build cloud-discord +on: + push: + branches: [ "**" ] + tags-ignore: [ "**" ] + pull_request: + release: + types: [ released ] +jobs: + build: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v4 + - uses: gradle/wrapper-validation-action@v1 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + - name: Build + run: ./gradlew build + - name : Test Summary + uses : EnricoMi/publish-unit-test-result-action@v2.11.0 + with : + junit_files : "**/build/test-results/test/TEST-*.xml" + if : always() + - name: Determine Status + run: | + if [ "$(./gradlew properties | awk '/^version:/ { print $2; }' | grep '\-SNAPSHOT')" ]; then + echo "STATUS=snapshot" >> $GITHUB_ENV + else + echo "STATUS=release" >> $GITHUB_ENV + fi + - name: Publish Snapshot + if: "${{ env.STATUS != 'release' && github.event_name == 'push' && github.ref == 'refs/heads/1.0.0-dev' }}" + run: ./gradlew publish + env: + ORG_GRADLE_PROJECT_sonatypeUsername: "${{ secrets.SONATYPE_USERNAME }}" + ORG_GRADLE_PROJECT_sonatypePassword: "${{ secrets.SONATYPE_PASSWORD }}" + - name: Publish Release + if: "${{ env.STATUS == 'release' && github.event_name == 'release' }}" + run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository + env: + ORG_GRADLE_PROJECT_sonatypeUsername: "${{ secrets.SONATYPE_USERNAME }}" + ORG_GRADLE_PROJECT_sonatypePassword: "${{ secrets.SONATYPE_PASSWORD }}" + ORG_GRADLE_PROJECT_signingKey: "${{ secrets.SIGNING_KEY }}" + ORG_GRADLE_PROJECT_signingPassword: "${{ secrets.SIGNING_PASSWORD }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaedbd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,190 @@ +# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,java,maven,gradle,kotlin,git +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,java,maven,gradle,kotlin,git + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Kotlin ### +# Compiled class file + +# Log file + +# BlueJ files + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +# JDT-specific (Eclipse Java Development Tools) + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/intellij+all,java,maven,gradle,kotlin,git \ No newline at end of file diff --git a/.spotless/cloud-discord.importorder b/.spotless/cloud-discord.importorder new file mode 100644 index 0000000..3eab772 --- /dev/null +++ b/.spotless/cloud-discord.importorder @@ -0,0 +1,3 @@ +# cloud-discord import order +0= +1=\# diff --git a/HEADER b/HEADER new file mode 100644 index 0000000..04f33e4 --- /dev/null +++ b/HEADER @@ -0,0 +1,23 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ac51a62 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Incendo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c94f84 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# cloud-discord + +Discord integrations for [Cloud v2](https://github.com/incendo/cloud). + +## modules + +- cloud-discord-common: shared utilities +- cloud-jda5: integration for JDA5 slash commands diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4dd4319 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + alias(libs.plugins.cloud.buildLogic.rootProject.publishing) + alias(libs.plugins.cloud.buildLogic.rootProject.spotless) +} + +spotlessPredeclare { + kotlin { ktlint(libs.versions.ktlint.get()) } + kotlinGradle { ktlint(libs.versions.ktlint.get()) } +} + +subprojects { + afterEvaluate { + tasks.withType().configureEach { + options.compilerArgs.remove("-Werror") + } + } +} + +tasks { + spotlessCheck { + dependsOn(gradle.includedBuild("build-logic").task(":spotlessCheck") ) + } + spotlessApply { + dependsOn(gradle.includedBuild("build-logic").task(":spotlessApply")) + } +} diff --git a/cloud-discord-common/README.md b/cloud-discord-common/README.md new file mode 100644 index 0000000..c450115 --- /dev/null +++ b/cloud-discord-common/README.md @@ -0,0 +1,7 @@ +# cloud-discord-common + +Shared utilities for Discord integrations. + +## slash commands + +cloud-discord-common contains tooling for mapping Cloud commands to Discord commands. diff --git a/cloud-discord-common/build.gradle.kts b/cloud-discord-common/build.gradle.kts new file mode 100644 index 0000000..614af4a --- /dev/null +++ b/cloud-discord-common/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("cloud-discord.base-conventions") + id("cloud-discord.publishing-conventions") +} + +dependencies { + api(libs.cloud.core) +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/ImmutableImpl.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/ImmutableImpl.java new file mode 100644 index 0000000..b7e84d3 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/ImmutableImpl.java @@ -0,0 +1,55 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.immutables; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apiguardian.api.API; +import org.immutables.value.Value; + +/** + * Annotation that generates immutable classes suffixed with "Impl". + */ +@Value.Style( + typeImmutable = "*Impl", + typeImmutableEnclosing = "*", + typeAbstract = "*", + deferCollectionAllocation = true, + optionalAcceptNullable = true, + jdkOnly = true, // We do not want any runtime dependencies! + allParameters = true, + headerComments = true, + jacksonIntegration = false, + visibility = Value.Style.ImplementationVisibility.PACKAGE, + builderVisibility = Value.Style.BuilderVisibility.PACKAGE, + defaults = @Value.Immutable(builder = false) +) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PACKAGE}) +@Retention(RetentionPolicy.SOURCE) +@API(status = API.Status.INTERNAL, since = "1.0.0") +public @interface ImmutableImpl { + +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/StagedImmutableBuilder.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/StagedImmutableBuilder.java new file mode 100644 index 0000000..a83571b --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/StagedImmutableBuilder.java @@ -0,0 +1,58 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.immutables; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apiguardian.api.API; +import org.immutables.value.Value; + +/** + * Annotation that generates immutables classes with staged builders. + */ +@Value.Style( + typeImmutableEnclosing = "*", + typeAbstract = "*", + deferCollectionAllocation = true, + optionalAcceptNullable = true, + jdkOnly = true, // We do not want any runtime dependencies! + allParameters = true, + headerComments = true, + jacksonIntegration = false, + builderVisibility = Value.Style.BuilderVisibility.SAME, + defaultAsDefault = true, + put = "*", + putAll = "*", + stagedBuilder = true, + depluralize = true, + depluralizeDictionary = "creationListeners:creationListener" +) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PACKAGE}) +@Retention(RetentionPolicy.SOURCE) +@API(status = API.Status.INTERNAL, since = "1.0.0") +public @interface StagedImmutableBuilder { + +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/package-info.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/package-info.java new file mode 100644 index 0000000..65a34e8 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/immutables/package-info.java @@ -0,0 +1,4 @@ +/** + * Configurations for {@link org.immutables.value.Value.Immutable}. + */ +package org.incendo.cloud.discord.immutables; diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/package-info.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/package-info.java new file mode 100644 index 0000000..273df8c --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/package-info.java @@ -0,0 +1 @@ +package org.incendo.cloud.discord; diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/CommandScope.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/CommandScope.java new file mode 100644 index 0000000..1c70553 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/CommandScope.java @@ -0,0 +1,183 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.Command; +import cloud.commandframework.keys.CloudKey; +import io.leangen.geantyref.TypeToken; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CommandScope extends Command.Builder.Applicable { + + CloudKey> META_COMMAND_SCOPE = CloudKey.of( + "cloud:command_scope", + new TypeToken>() { + } + ); + + /** + * Returns the global command scope. + * + * @param command sender type + * @return global scope + */ + @SuppressWarnings("unchecked") + static @NonNull CommandScope global() { + return (CommandScope) Global.GLOBAL; + } + + /** + * Returns a command scope for the given {@code guilds}. + * + * @param command sender type + * @param guilds the guilds + * @return guild scope + */ + static @NonNull CommandScope guilds(final long @NonNull... guilds) { + return new Guilds<>(guilds); + } + + /** + * Returns a command scope that makes the command be active in all guilds. + * + * @param command sender type + * @return guild scope + */ + static @NonNull CommandScope guilds() { + return new Guilds<>(-1); + } + + /** + * Checks if there's any overlap between {@code this} scope and the given {@code scope}. + * + * @param scope other scope + * @return {@code true} if the scopes overlap, else {@code false} + */ + boolean overlaps(@NonNull CommandScope scope); + + @Override + default Command.@NonNull Builder applyToCommandBuilder(Command.@NonNull Builder builder) { + return builder.meta(META_COMMAND_SCOPE, this); + } + + /** + * Makes the command globally available. + * + * @param command sender type + * @since 1.0.0 + */ + @API(status = API.Status.STABLE, since = "1.0.0") + final class Global implements CommandScope { + + private static final Global GLOBAL = new Global<>(); + + private Global() { + } + + @Override + public boolean overlaps(final @NonNull CommandScope scope) { + return true; + } + } + + /** + * Makes the command available in specific guilds. + * + * @param command sender type + * @since 1.0.0 + */ + @API(status = API.Status.STABLE, since = "1.0.0") + final class Guilds implements CommandScope { + + private final Set guilds; + + private Guilds(final @NonNull Set<@NonNull Long> guilds) { + this.guilds = Collections.unmodifiableSet(guilds); + } + + private Guilds(final long @NonNull... guilds) { + this(Arrays.stream(guilds).boxed().collect(Collectors.toSet())); + } + + private Guilds(final long guildId) { + this.guilds = Collections.singleton(guildId); + } + + /** + * Returns an unmodifiable view of the guilds. + * + * @return the guilds + */ + public @NonNull Set<@NonNull Long> guilds() { + return this.guilds; + } + + /** + * Returns a new {@link Guilds} instance with the given {@code guildId} added. + * + * @param guildId guild to add + * @return the new instance + */ + public @NonNull Guilds withGuild(final long guildId) { + final Set guilds = new HashSet<>(this.guilds); + guilds.add(guildId); + return new Guilds<>(guilds); + } + + /** + * Returns a new {@link Guilds} instance with the given {@code guilds} added. + * + * @param guilds new guilds to add + * @return the new instance + */ + public @NonNull Guilds withGuild(final Set<@NonNull Long> guilds) { + final Set newGuilds = new HashSet<>(this.guilds); + newGuilds.addAll(guilds); + return new Guilds<>(newGuilds); + } + + @Override + public boolean overlaps(final @NonNull CommandScope scope) { + if (!(scope instanceof Guilds)) { + return false; + } + final Guilds guilds = (Guilds) scope; + return this.guilds.stream().anyMatch(guildId -> guilds.guilds().contains(guildId)); + } + + @Override + public @NonNull String toString() { + return "Guilds{" + + "guilds=" + this.guilds + + '}'; + } + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoiceProvider.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoiceProvider.java new file mode 100644 index 0000000..6f3ff8b --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoiceProvider.java @@ -0,0 +1,52 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.arguments.suggestion.BlockingSuggestionProvider; +import cloud.commandframework.arguments.suggestion.Suggestion; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandInput; +import java.util.ArrayList; +import java.util.Collection; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +@API(status = API.Status.STABLE, since = "1.0.0") +public interface DiscordChoiceProvider extends BlockingSuggestionProvider { + + @Override + default @NonNull Iterable<@NonNull Suggestion> suggestions( + final @NonNull CommandContext context, + final @NonNull CommandInput input + ) { + return new ArrayList<>(this.choices()); + } + + /** + * Returns the choices. + * + * @return the choices + */ + @NonNull Collection<@NonNull DiscordOptionChoice> choices(); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoices.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoices.java new file mode 100644 index 0000000..bd4bce1 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordChoices.java @@ -0,0 +1,157 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; +import org.incendo.cloud.discord.immutables.ImmutableImpl; + +@ImmutableImpl +@Value.Immutable +@SuppressWarnings("varargs") +@API(status = API.Status.STABLE, since = "1.0.0 ") +public interface DiscordChoices extends DiscordChoiceProvider { + + /** + * Creates a {@link DiscordChoices} instance from the given {@code choices}. + * + * @param command sender type + * @param choice type + * @param choices choices + * @return the created instance + */ + static @NonNull DiscordChoices choices(final @NonNull Collection<@NonNull DiscordOptionChoice> choices) { + return DiscordChoicesImpl.of(choices); + } + + /** + * Creates a {@link DiscordChoices} instance from the given {@code choices}. + * + * @param command sender type + * @param choice type + * @param choices choices + * @return the created instance + */ + @SafeVarargs + static @NonNull DiscordChoices choices(final @NonNull DiscordOptionChoice @NonNull... choices) { + return DiscordChoicesImpl.of(Arrays.asList(choices)); + } + + /** + * Creates a {@link DiscordChoices} instance from the given {@code choices}. + * + * @param command sender type + * @param choices choices + * @return the created instance + */ + static @NonNull DiscordChoices integers(final @NonNull Collection<@NonNull Integer> choices) { + return DiscordChoicesImpl.of( + choices.stream() + .map(integer -> DiscordOptionChoice.of(Integer.toString(integer), integer)) + .collect(Collectors.toList()) + ); + } + + /** + * Creates a {@link DiscordChoices} instance from the given {@code choices}. + * + * @param command sender type + * @param choices choices + * @return the created instance + */ + static @NonNull DiscordChoices integers(final int @NonNull... choices) { + return DiscordChoicesImpl.of( + Arrays.stream(choices) + .mapToObj(integer -> DiscordOptionChoice.of(Integer.toString(integer), integer)) + .collect(Collectors.toList()) + ); + } + + /** + * Creates a {@link DiscordChoices} instance from the given {@code choices}. + * + * @param command sender type + * @param choices choices + * @return the created instance + */ + static @NonNull DiscordChoices doubles(final @NonNull Collection<@NonNull Double> choices) { + return DiscordChoicesImpl.of( + choices.stream() + .map(number -> DiscordOptionChoice.of(Double.toString(number), number)) + .collect(Collectors.toList()) + ); + } + + /** + * Creates a {@link DiscordChoices} instance from the given {@code choices}. + * + * @param command sender type + * @param choices choices + * @return the created instance + */ + static @NonNull DiscordChoices doubles(final double @NonNull... choices) { + return DiscordChoicesImpl.of( + Arrays.stream(choices) + .mapToObj(number -> DiscordOptionChoice.of(Double.toString(number), number)) + .collect(Collectors.toList()) + ); + } + + /** + * Creates a {@link DiscordChoices} instance from the given {@code choices}. + * + * @param command sender type + * @param choices choices + * @return the created instance + */ + static @NonNull DiscordChoices strings(final @NonNull Collection<@NonNull String> choices) { + return DiscordChoicesImpl.of( + choices.stream() + .map(string -> DiscordOptionChoice.of(string, string)) + .collect(Collectors.toList()) + ); + } + + /** + * Creates a {@link DiscordChoices} instance from the given {@code choices}. + * + * @param command sender type + * @param choices choices + * @return the created instance + */ + static @NonNull DiscordChoices strings(final @NonNull String @NonNull... choices) { + return DiscordChoicesImpl.of( + Arrays.stream(choices) + .map(string -> DiscordOptionChoice.of(string, string)) + .collect(Collectors.toList()) + ); + } + + @Override + @NonNull Collection<@NonNull DiscordOptionChoice> choices(); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommand.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommand.java new file mode 100644 index 0000000..eca8184 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommand.java @@ -0,0 +1,74 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import java.util.List; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; +import org.incendo.cloud.discord.immutables.StagedImmutableBuilder; + +/** + * Represents a Discord command. + * + * @param command sender type + * @since 1.0.0 + */ +@StagedImmutableBuilder +@Value.Immutable +@API(status = API.Status.STABLE, since = "1.0.0") +public interface DiscordCommand extends Named { + + /** + * Creates a new command builder. + * + * @param command sender type + * @return the builder + */ + static ImmutableDiscordCommand.@NonNull NameBuildStage builder() { + return ImmutableDiscordCommand.builder(); + } + + /** + * Returns the command name. + * + * @return the name + */ + @Override + @NonNull String name(); + + /** + * Returns the command description. + * + * @return the description + */ + @NonNull String description(); + + /** + * Returns the command options. + * + * @return the options + */ + @NonNull List<@NonNull DiscordOption> options(); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommandFactory.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommandFactory.java new file mode 100644 index 0000000..31d4dca --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordCommandFactory.java @@ -0,0 +1,40 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.internal.CommandNode; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +@API(status = API.Status.STABLE, since = "1.0.0") +public interface DiscordCommandFactory { + + /** + * Creates the command that represent the given {@code node}. + * + * @param node cloud node + * @return the option + */ + @NonNull DiscordCommand create(@NonNull CommandNode node); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOption.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOption.java new file mode 100644 index 0000000..68585bb --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOption.java @@ -0,0 +1,136 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.Command; +import java.util.List; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.immutables.value.Value; +import org.incendo.cloud.discord.immutables.StagedImmutableBuilder; + +/** + * Represents a Discord option. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface DiscordOption extends Named { + + /** + * Returns the option name. + * + *

Must be between 1 and 32 characters.

+ * + * @return the name + */ + @Override + @NonNull String name(); + + /** + * Returns the option description. + * + *

Must be between 1 and 100 characters.

+ * + * @return the description + */ + @NonNull String description(); + + /** + * Returns the type of the option. + * + * @return option type + */ + @NonNull DiscordOptionType type(); + + /** + * Returns the associated command. + * + * @return the command, or {@code null} + */ + @Nullable Command command(); + + + @SuppressWarnings("immutables:subtype") + @StagedImmutableBuilder + @Value.Immutable + interface SubCommand extends DiscordOption { + + @Value.Derived + @Override + default @NonNull DiscordOptionType type() { + if (!this.options().isEmpty() && this.options().get(0) instanceof SubCommand) { + return DiscordOptionType.SUB_COMMAND_GROUP; + } + return DiscordOptionType.SUB_COMMAND; + } + + /** + * Returns the child options. + * + * @return the options + */ + @NonNull List<@NonNull DiscordOption> options(); + } + + @SuppressWarnings("immutables:subtype") + @StagedImmutableBuilder + @Value.Immutable + interface Variable extends DiscordOption { + + /** + * Whether the option is required. + * + * @return whether the option is required + */ + boolean required(); + + /** + * Whether the autocomplete interaction is enabled for this option. + * + * @return whether autocomplete is enabled + */ + boolean autocomplete(); + + /** + * Returns the choices. + * + *

This is only relevant for STRING, INTEGER & NUMBER options.

+ * + * @return the choices + */ + @NonNull List<@NonNull DiscordOptionChoice> choices(); + + /** + * Returns the range, if applicable. + * + * @return the range, or {@code null} + */ + default @Nullable Range range() { + return null; + } + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionChoice.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionChoice.java new file mode 100644 index 0000000..2c49ad4 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionChoice.java @@ -0,0 +1,79 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.arguments.suggestion.Suggestion; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; +import org.incendo.cloud.discord.immutables.ImmutableImpl; + +/** + * Represents a Discord option choice. + * + * @param option type + * @since 1.0.0 + */ +@ImmutableImpl +@Value.Immutable +@API(status = API.Status.STABLE, since = "1.0.0") +public interface DiscordOptionChoice extends Named, Suggestion { + + /** + * Creates a new choice. + * + * @param option type + * @param name choice name + * @param value choice value + * @return the choice + */ + static @NonNull DiscordOptionChoice of(final @NonNull String name, final @NonNull T value) { + return DiscordOptionChoiceImpl.of(name, value); + } + + /** + * Returns the choice name. + * + * @return the name + */ + @Override + @NonNull String name(); + + /** + * Returns the choice value. + * + * @return the value + */ + @NonNull T value(); + + @Override + default @NonNull String suggestion() { + return this.name(); + } + + @Override + default @NonNull DiscordOptionChoice withSuggestion(final @NonNull String suggestion) { + return DiscordOptionChoiceImpl.copyOf(this).withSuggestion(suggestion); + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionType.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionType.java new file mode 100644 index 0000000..a529cd2 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordOptionType.java @@ -0,0 +1,101 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import io.leangen.geantyref.TypeToken; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; +import org.incendo.cloud.discord.immutables.ImmutableImpl; + +/** + * Represents a Discord option type. + * + * @param option type + * @since 1.0.0 + */ +@Value.Immutable +@ImmutableImpl +@API(status = API.Status.STABLE, since = "1.0.0") +public interface DiscordOptionType<@NonNull T> extends Named { + + @NonNull DiscordOptionType> SUB_COMMAND = of("SUB_COMMAND", 1, new TypeToken>() { + }); + @NonNull DiscordOptionType>> SUB_COMMAND_GROUP = of("SUB_COMMAND_GROUP", 2, + new TypeToken>>() { + } + ); + @NonNull DiscordOptionType STRING = of("STRING", 3, TypeToken.get(String.class)); + @NonNull DiscordOptionType INTEGER = of("INTEGER", 4, TypeToken.get(Integer.class)); + @NonNull DiscordOptionType BOOLEAN = of("BOOLEAN", 5, TypeToken.get(Boolean.class)); + @NonNull DiscordOptionType NUMBER = of("NUMBER", 10, TypeToken.get(Double.class)); + // Non-generic types must be implemented in the platform modules. + + @NonNull Collection<@NonNull DiscordOptionType> AUTOCOMPLETE = Collections.unmodifiableCollection( + Arrays.asList(STRING, INTEGER, NUMBER) + ); + + /** + * Creates a new option instance. + * + * @param type represented by the option + * @param name option name + * @param value option value + * @param type option type + * @return the option + */ + static @NonNull DiscordOptionType of( + final @NonNull String name, + final int value, + final TypeToken type + ) { + return DiscordOptionTypeImpl.of(name, value, type); + } + + /** + * Returns the name of the option type. + * + * @return option type name + */ + @Override + @NonNull String name(); + + /** + * Returns the option value. + * + * @return option value + */ + int value(); + + /** + * Returns the option type. + * + * @return option type + */ + @NonNull TypeToken type(); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordPermission.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordPermission.java new file mode 100644 index 0000000..541084e --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordPermission.java @@ -0,0 +1,72 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.permission.Permission; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +@API(status = API.Status.STABLE, since = "1.0.0") +public final class DiscordPermission implements Permission { + + /** + * Creates a new Discord permission. + * + * @param permission permission + * @return the created permission + */ + public static @NonNull DiscordPermission of(final long permission) { + return new DiscordPermission(permission); + } + + /** + * Creates a new Discord permission. + * + * @param permission permission + * @return the created permission + */ + public static @NonNull DiscordPermission discordPermission(final long permission) { + return new DiscordPermission(permission); + } + + private final long permission; + + private DiscordPermission(final long permission) { + this.permission = permission; + } + + @Override + public @NonNull String permissionString() { + return Long.toString(this.permission); + } + + /** + * Returns the permission. + * + * @return permission + */ + public long permission() { + return this.permission; + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordSetting.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordSetting.java new file mode 100644 index 0000000..00959a1 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/DiscordSetting.java @@ -0,0 +1,52 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.setting.Setting; +import org.apiguardian.api.API; + +/** + * Discord-specific settings. + * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public enum DiscordSetting implements Setting { + /** + * Whether slash commands should be registered automatically. + */ + AUTO_REGISTER_SLASH_COMMANDS, + /** + * Whether error messages should be ephemeral. + */ + EPHEMERAL_ERROR_MESSAGES, + /** + * Always defer replies. + */ + FORCE_DEFER_NON_EPHEMERAL, + /** + * Always defer replies. + */ + FORCE_DEFER_EPHEMERAL +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Named.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Named.java new file mode 100644 index 0000000..15932d7 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Named.java @@ -0,0 +1,43 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Something with a name. + * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface Named { + + /** + * Returns the name. + * + * @return the name + */ + @NonNull String name(); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/NodeProcessor.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/NodeProcessor.java new file mode 100644 index 0000000..0daf2a3 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/NodeProcessor.java @@ -0,0 +1,113 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.CommandTree; +import cloud.commandframework.internal.CommandNode; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Processes {@link CommandNode nodes} and prepares them for mapping to Discord commands. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +public final class NodeProcessor { + + public static final String NODE_META_SCOPE = "scope"; + + private final CommandTree commandTree; + + /** + * Creates a new node processor. + * + * @param commandTree tree that should be processed + */ + public NodeProcessor(final @NonNull CommandTree commandTree) { + this.commandTree = commandTree; + } + + /** + * Prepares the command tree. + */ + public void prepareTree() { + this.commandTree.getLeavesRaw(this.commandTree.rootNode()) + .forEach(this::propagateRequirements); + } + + @SuppressWarnings("unchecked") + private void propagateRequirements(final @NonNull CommandNode leafNode) { + CommandScope parentScope = (CommandScope) leafNode.command().commandMeta().getOrDefault( + CommandScope.META_COMMAND_SCOPE, + CommandScope.global() + ); + leafNode.nodeMeta().put(NODE_META_SCOPE, parentScope); + + List> chain = this.getChain(leafNode); + Collections.reverse(chain); + chain = chain.subList(1, chain.size()); + + for (final CommandNode commandNode : chain) { + final CommandScope existingScope = (CommandScope) commandNode.nodeMeta().get(NODE_META_SCOPE); + + CommandScope scope; + if (existingScope != null) { + // Global scope overrides everything :) + if (existingScope instanceof CommandScope.Global) { + scope = existingScope; + } else if (existingScope instanceof CommandScope.Guilds) { + if (parentScope instanceof CommandScope.Guilds) { + scope = ((CommandScope.Guilds) existingScope) + .withGuild(((CommandScope.Guilds) parentScope).guilds()); + } else { + scope = existingScope; + } + } else { + scope = parentScope; + } + } else { + scope = parentScope; + } + + commandNode.nodeMeta().put(NODE_META_SCOPE, scope); + } + } + + private @NonNull List<@NonNull CommandNode> getChain(final @Nullable CommandNode end) { + final List> chain = new LinkedList<>(); + CommandNode tail = end; + while (tail != null) { + chain.add(tail); + tail = tail.parent(); + } + Collections.reverse(chain); + return chain; + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/OptionRegistry.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/OptionRegistry.java new file mode 100644 index 0000000..b514038 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/OptionRegistry.java @@ -0,0 +1,97 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.arguments.parser.ParserDescriptor; +import io.leangen.geantyref.TypeToken; +import java.util.Collection; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.returnsreceiver.qual.This; + +/** + * Registry that stores {@link DiscordOptionType}-{@link ParserDescriptor} mappings. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface OptionRegistry { + + /** + * Registers a mapping between the given {@code optionType} and the given {@code parser}. + * + * @param optionType discord option type + * @param parser cloud parser + * @return {@code this} + */ + @This @NonNull OptionRegistry registerMapping( + @NonNull DiscordOptionType optionType, + @NonNull ParserDescriptor parser + ); + + /** + * Returns the option type that best corresponds to the given {@code valueType}. + * + * @param valueType type to get the option for + * @return the best matching option, using {@link DiscordOptionType#STRING} as the fallback + */ + @NonNull DiscordOptionType getOption(@NonNull TypeToken valueType); + + /** + * Returns an unmodifiable view of the recognized option types. + * + * @return option types + */ + @NonNull Collection<@NonNull DiscordOptionType> optionTypes(); + + /** + * Returns the option type with the given {@code value}, if it exists. + * + * @param value value to get the option by + * @return the option type, or {@code null} + */ + default @Nullable DiscordOptionType getByValue(final int value) { + return this.optionTypes() + .stream() + .filter(option -> option.value() == value) + .findFirst() + .orElse(null); + } + + /** + * Returns the option type with the given {@code name}, if it exists. + * + * @param name name to get the option by + * @return the option type, or {@code null} + */ + default @Nullable DiscordOptionType getByName(final @NonNull String name) { + return this.optionTypes() + .stream() + .filter(option -> option.name().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Range.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Range.java new file mode 100644 index 0000000..cfa3a7d --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/Range.java @@ -0,0 +1,60 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; +import org.incendo.cloud.discord.immutables.ImmutableImpl; + +@ImmutableImpl +@Value.Immutable +@API(status = API.Status.STABLE, since = "1.0.0") +public interface Range { + + /** + * Creates a new range. + * + * @param min minimum value + * @param max maximum value + * @return the range + */ + static @NonNull Range of(final @NonNull Number min, final @NonNull Number max) { + return RangeImpl.of(min, max); + } + + /** + * Returns the minimum value. + * + * @return the min value + */ + @NonNull Number min(); + + /** + * Returns the maximum value. + * + * @return the max value + */ + @NonNull Number max(); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/RangeMapper.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/RangeMapper.java new file mode 100644 index 0000000..89e71df --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/RangeMapper.java @@ -0,0 +1,49 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.arguments.parser.ArgumentParser; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Maps min-max values to a {@link Range}. + * + * @param command sender type + * @param parser value type + * @param

parser type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface RangeMapper> { + + /** + * Maps the given {@code parser} to a range. + * + * @param parser parser to map + * @return the range, or {@code null} if no range could be extracted + */ + @Nullable Range map(@NonNull P parser); +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardDiscordCommandFactory.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardDiscordCommandFactory.java new file mode 100644 index 0000000..875a642 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardDiscordCommandFactory.java @@ -0,0 +1,227 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.CommandComponent; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.arguments.standard.ByteParser; +import cloud.commandframework.arguments.standard.DoubleParser; +import cloud.commandframework.arguments.standard.FloatParser; +import cloud.commandframework.arguments.standard.IntegerParser; +import cloud.commandframework.arguments.standard.LongParser; +import cloud.commandframework.arguments.standard.ShortParser; +import cloud.commandframework.arguments.suggestion.SuggestionProvider; +import cloud.commandframework.internal.CommandNode; +import io.leangen.geantyref.GenericTypeReflector; +import io.leangen.geantyref.TypeToken; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +@API(status = API.Status.INTERNAL, since = "1.0.0") +public class StandardDiscordCommandFactory implements DiscordCommandFactory { + + private final OptionRegistry optionRegistry; + private final Map, RangeMapper> rangeMappers = new HashMap<>(); + + /** + * Creates a new factory instance. + * + * @param optionRegistry option registry to retrieve option types from + */ + public StandardDiscordCommandFactory(final @NonNull OptionRegistry optionRegistry) { + this.optionRegistry = optionRegistry; + this.registerRangeMapper(new TypeToken>() { + }, parser -> { + if (!parser.hasMin() && !parser.hasMax()) { + return null; + } + return Range.of(parser.min(), parser.max()); + }); + this.registerRangeMapper(new TypeToken>() { + }, parser -> { + if (!parser.hasMin() && !parser.hasMax()) { + return null; + } + return Range.of(parser.min(), parser.max()); + }); + this.registerRangeMapper(new TypeToken>() { + }, parser -> { + if (!parser.hasMin() && !parser.hasMax()) { + return null; + } + return Range.of(parser.min(), parser.max()); + }); + this.registerRangeMapper(new TypeToken>() { + }, parser -> { + if (!parser.hasMin() && !parser.hasMax()) { + return null; + } + return Range.of(parser.min(), parser.max()); + }); + this.registerRangeMapper(new TypeToken>() { + }, parser -> { + if (!parser.hasMin() && !parser.hasMax()) { + return null; + } + return Range.of(parser.min(), parser.max()); + }); + this.registerRangeMapper(new TypeToken>() { + }, parser -> { + if (!parser.hasMin() && !parser.hasMax()) { + return null; + } + return Range.of(parser.min(), parser.max()); + }); + } + + /** + * Registers the given range {@code mapper}. + * + * @param type produced by parser + * @param

parser type + * @param parserClass parser class + * @param mapper range mapper + */ + public > void registerRangeMapper( + final @NonNull TypeToken

parserClass, + final @NonNull RangeMapper mapper + ) { + this.rangeMappers.put(GenericTypeReflector.erase(parserClass.getType()), mapper); + } + + @Override + public @NonNull DiscordCommand create(final @NonNull CommandNode node) { + final CommandComponent component = node.component(); + final List> options = new ArrayList<>(); + + CommandNode currentNode = node; + while (currentNode != null) { + boolean subCommand = false; + for (final CommandNode child : currentNode.children()) { + final DiscordOption childOption = this.createOption(child); + subCommand = subCommand || childOption instanceof DiscordOption.SubCommand; + options.add(childOption); + } + + // If we encountered a subcommand or a subcommand group, then we let the subcommand deal with the + // options for us. Otherwise, we keep iterating until we've constructed the tree. + if (subCommand) { + break; + } + + if (currentNode.isLeaf()) { + currentNode = null; + } else { + currentNode = currentNode.children().get(0); + } + } + + return DiscordCommand.builder() + .name(component.name()) + .description(node.component().description().textDescription()) + .addAllOptions(options) + .build(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private @NonNull DiscordOption createOption(final @NonNull CommandNode node) { + final CommandComponent component = node.component(); + + if (component.type() == CommandComponent.ComponentType.LITERAL) { + // We need to determine whether to flatten the children into a sub-command + // or whether to recursively extract the arguments. + List> children = new ArrayList<>(node.children() + .stream() + .map(this::createOption) + .collect(Collectors.toList())); + + // If there's only one child and the child isn't a sub-command, then we recursively find the children. + if (children.size() == 1 && children.get(0) instanceof DiscordOption.Variable) { + children.clear(); + + CommandNode child = node.children().get(0); + while (child != null) { + children.add(this.createOption(child)); + + if (child.isLeaf()) { + child = null; + } else { + child = child.children().get(0); + } + } + } + + return ImmutableSubCommand.builder() + .name(component.name()) + .description(component.description().textDescription()) + .addAllOptions(children) + .build(); + } + + final DiscordOptionType optionType = this.optionRegistry.getOption(component.valueType()); + final Collection choices = this.extractChoices(component.suggestionProvider()); + final Range range = this.extractRange(component.parser()); + + final boolean autoComplete; + if (choices.isEmpty()) { + autoComplete = DiscordOptionType.AUTOCOMPLETE.contains(optionType); + } else { + autoComplete = false; + } + + return ImmutableVariable.builder().name(component.name()).description(component.description().textDescription()) + .type(optionType) + .required(component.required()) + .autocomplete(autoComplete) + .addAllChoices(choices) + .range(range) + .build(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private @Nullable Range extractRange(final @NonNull ArgumentParser parser) { + final RangeMapper rangeMapper = this.rangeMappers.get(parser.getClass()); + if (rangeMapper == null) { + return null; + } + return rangeMapper.map(parser); + } + + private @NonNull Collection> extractChoices( + final @NonNull SuggestionProvider suggestionProvider + ) { + if (!(suggestionProvider instanceof DiscordChoiceProvider)) { + return Collections.emptyList(); + } + return ((DiscordChoiceProvider) suggestionProvider).choices(); + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardOptionRegistry.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardOptionRegistry.java new file mode 100644 index 0000000..3466f84 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/StandardOptionRegistry.java @@ -0,0 +1,83 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.slash; + +import cloud.commandframework.arguments.parser.ParserDescriptor; +import cloud.commandframework.arguments.standard.BooleanParser; +import cloud.commandframework.arguments.standard.DoubleParser; +import cloud.commandframework.arguments.standard.IntegerParser; +import cloud.commandframework.arguments.standard.StringParser; +import io.leangen.geantyref.AnnotatedTypeMap; +import io.leangen.geantyref.TypeToken; +import java.lang.reflect.AnnotatedType; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; + +/** + * Standard implementation of {@link OptionRegistry}. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public final class StandardOptionRegistry implements OptionRegistry { + + private final Map, ParserDescriptor> parserMap = new HashMap<>(); + private final Map> optionMap = new AnnotatedTypeMap<>(); + + /** + * Creates a new standard option registry. + */ + public StandardOptionRegistry() { + this.registerMapping(DiscordOptionType.STRING, StringParser.stringParser()) + .registerMapping(DiscordOptionType.INTEGER, IntegerParser.integerParser()) + .registerMapping(DiscordOptionType.BOOLEAN, BooleanParser.booleanParser()) + .registerMapping(DiscordOptionType.NUMBER, DoubleParser.doubleParser()); + } + + @Override + public @This @NonNull OptionRegistry registerMapping( + final @NonNull DiscordOptionType optionType, + final @NonNull ParserDescriptor parser + ) { + this.parserMap.put(optionType, parser); + this.optionMap.put(parser.valueType().getAnnotatedType(), optionType); + return this; + } + + @Override + public @NonNull DiscordOptionType getOption(final @NonNull TypeToken valueType) { + return this.optionMap.getOrDefault(valueType.getAnnotatedType(), DiscordOptionType.STRING); + } + + @Override + public @NonNull Collection<@NonNull DiscordOptionType> optionTypes() { + return Collections.unmodifiableCollection(this.parserMap.keySet()); + } +} diff --git a/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/package-info.java b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/package-info.java new file mode 100644 index 0000000..2890ad5 --- /dev/null +++ b/cloud-discord-common/src/main/java/org/incendo/cloud/discord/slash/package-info.java @@ -0,0 +1 @@ +package org.incendo.cloud.discord.slash; diff --git a/cloud-jda5/README.md b/cloud-jda5/README.md new file mode 100644 index 0000000..ea1e028 --- /dev/null +++ b/cloud-jda5/README.md @@ -0,0 +1,3 @@ +# cloud-jda5 + +Cloud integration for JDA5 slash commands. diff --git a/cloud-jda5/build.gradle.kts b/cloud-jda5/build.gradle.kts new file mode 100644 index 0000000..9e80caa --- /dev/null +++ b/cloud-jda5/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("cloud-discord.base-conventions") + id("cloud-discord.publishing-conventions") +} + +dependencies { + api(projects.cloudDiscordCommon) + implementation(libs.jda) +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/CommandListener.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/CommandListener.java new file mode 100644 index 0000000..4f394de --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/CommandListener.java @@ -0,0 +1,126 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import java.util.stream.Collectors; +import net.dv8tion.jda.api.events.guild.GuildReadyEvent; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.session.ReadyEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.CommandInteractionPayload; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.slash.DiscordSetting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class CommandListener extends ListenerAdapter { + + private final Logger logger = LoggerFactory.getLogger(CommandListener.class); + + private final JDA5CommandManager commandManager; + + CommandListener(final @NonNull JDA5CommandManager commandManager) { + this.commandManager = commandManager; + } + + @Override + public void onGuildReady(final @NonNull GuildReadyEvent event) { + if (!this.commandManager.discordSettings().get(DiscordSetting.AUTO_REGISTER_SLASH_COMMANDS)) { + return; + } + + this.commandManager.registerGuildCommands(event.getGuild()); + } + + @Override + public void onReady(final @NonNull ReadyEvent event) { + if (!this.commandManager.discordSettings().get(DiscordSetting.AUTO_REGISTER_SLASH_COMMANDS)) { + return; + } + + this.commandManager.registerGlobalCommands(event.getJDA()); + } + + @Override + public void onSlashCommandInteraction(final @NonNull SlashCommandInteractionEvent event) { + final JDAInteraction interaction = JDAInteraction.builder() + .user(event.getUser()) + .guild(event.getGuild()) + .replyCallback(event) + .interactionEvent(event) + .build(); + this.commandManager.commandExecutor().executeCommand( + this.commandManager.senderMapper().map(interaction), + this.extractCommandName(event), + context -> context.store(JDA5CommandManager.CONTEXT_JDA_INTERACTION, interaction) + ); + } + + @Override + public void onCommandAutoCompleteInteraction(final @NonNull CommandAutoCompleteInteractionEvent event) { + String commandName = this.extractCommandName(event); + + final String value = event.getFocusedOption().getValue(); + if (value.isEmpty()) { + commandName = commandName + ' '; + } + + final JDAInteraction interaction = JDAInteraction.builder() + .user(event.getUser()) + .guild(event.getGuild()) + .replyCallback(null) + .interactionEvent(null) + .build(); + event.replyChoices( + this.commandManager.suggestionFactory().suggestImmediately( + this.commandManager.senderMapper().map(interaction), + commandName + ).list() + .stream() + .map(suggestion -> { + switch (event.getFocusedOption().getType()) { + case INTEGER: + return new Command.Choice(suggestion.suggestion(), Integer.parseInt(suggestion.suggestion())); + case NUMBER: + return new Command.Choice(suggestion.suggestion(), + Double.parseDouble(suggestion.suggestion())); + default: + return new Command.Choice(suggestion.suggestion(), suggestion.suggestion()); + } + + }) + .collect(Collectors.toList()) + ).queue(); + } + + private @NonNull String extractCommandName(final @NonNull CommandInteractionPayload payload) { + final StringBuilder command = new StringBuilder(payload.getFullCommandName()); + payload.getOptions().forEach(option -> { + command.append(" ").append(option.getAsString()); + }); + return command.toString(); + } +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDA5CommandManager.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDA5CommandManager.java new file mode 100644 index 0000000..4623f83 --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDA5CommandManager.java @@ -0,0 +1,211 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.exceptions.CommandExecutionException; +import cloud.commandframework.exceptions.InvalidCommandSenderException; +import cloud.commandframework.exceptions.InvalidSyntaxException; +import cloud.commandframework.exceptions.NoPermissionException; +import cloud.commandframework.exceptions.NoSuchCommandException; +import cloud.commandframework.execution.ExecutionCoordinator; +import cloud.commandframework.internal.CommandRegistrationHandler; +import cloud.commandframework.keys.CloudKey; +import cloud.commandframework.setting.Configurable; +import io.leangen.geantyref.TypeToken; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.hooks.EventListener; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.slash.CommandScope; +import org.incendo.cloud.discord.slash.DiscordSetting; + +/** + * Command manager for JDA5. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public class JDA5CommandManager extends CommandManager { + + public static final CloudKey CONTEXT_JDA_INTERACTION = CloudKey.of( + "cloud:jda_interaction", + JDAInteraction.class + ); + public static final CloudKey> META_REPLY_SETTING = CloudKey.of( + "cloud:reply_setting", + new TypeToken>() { + } + ); + + private final JDAInteraction.InteractionMapper senderMapper; + private final Configurable discordSettings; + + private BiPredicate permissionPredicate; + private JDACommandFactory commandFactory; + + /** + * Creates a new command manager. + * + * @param executionCoordinator execution coordinator instance + * @param senderMapper mapper from {@link JDAInteraction} to {@link C} + */ + public JDA5CommandManager( + final @NonNull ExecutionCoordinator executionCoordinator, + final JDAInteraction.@NonNull InteractionMapper senderMapper + ) { + super(executionCoordinator, CommandRegistrationHandler.nullCommandRegistrationHandler()); + this.commandFactory = new StandardJDACommandFactory<>(this.commandTree()); + this.discordSettings = Configurable.enumConfigurable(DiscordSetting.class); + this.permissionPredicate = (sender, permission) -> true; + this.senderMapper = senderMapper; + this.registerCommandPostProcessor(new ReplyCommandPostprocessor<>(this)); + + this.discordSettings.set(DiscordSetting.AUTO_REGISTER_SLASH_COMMANDS, true); + this.registerDefaultExceptionHandlers(); + } + + @Override + public boolean hasPermission(final @NonNull C sender, final @NonNull String permission) { + return this.permissionPredicate.test(sender, permission); + } + + /** + * Returns the command factory. + * + * @return the command factory + */ + public final @NonNull JDACommandFactory commandFactory() { + return this.commandFactory; + } + + /** + * Sets the command factory. + * + * @param commandFactory command factory + */ + public final void commandFactory(final @NonNull JDACommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + /** + * Returns the mapper that maps from the Discord {@link User} to the sender of type {@link C}. + * + * @return the sender mapper + */ + public final JDAInteraction.@NonNull InteractionMapper senderMapper() { + return this.senderMapper; + } + + /** + * Creates an event listener. + * + * @return the listener + */ + public final @NonNull EventListener createListener() { + return new CommandListener<>(this); + } + + /** + * Returns the Discord settings. + * + * @return discord settings + */ + public final @NonNull Configurable discordSettings() { + return this.discordSettings; + } + + /** + * Sets the permission predicate. + * + * @param permissionPredicate permission predicate + */ + public final void permissionPredicate(final @NonNull BiPredicate permissionPredicate) { + this.permissionPredicate = permissionPredicate; + } + + /** + * Registers global commands. + * + * @param jda JDA instance + */ + public void registerGlobalCommands(final @NonNull JDA jda) { + jda.updateCommands() + .addCommands(this.commandFactory.createCommands(CommandScope.global())) + .queue(); + } + + /** + * Registers guild commands. + * + * @param guild guild to register commands to + */ + public void registerGuildCommands(final @NonNull Guild guild) { + guild.updateCommands() + .addCommands(this.commandFactory.createCommands(CommandScope.guilds(-1, guild.getIdLong()))) + .queue(); + } + + @SuppressWarnings("unchecked") + private void registerDefaultExceptionHandlers() { + final BiConsumer, String> sendMessage = (context, message) -> { + final JDAInteraction interaction = context.get(CONTEXT_JDA_INTERACTION); + final ReplySetting replySetting = (ReplySetting) context + .getOrDefault(META_REPLY_SETTING, null); + + if (replySetting == null && this.discordSettings().get(DiscordSetting.EPHEMERAL_ERROR_MESSAGES)) { + interaction.replyCallback().deferReply(true).queue(); + interaction.interactionEvent().getHook().sendMessage(message).queue(); + } else if (replySetting != null && replySetting.defer()) { + interaction.interactionEvent().getHook().sendMessage(message).queue(); + } else { + interaction.replyCallback().reply(message).queue(); + } + }; + + this.exceptionController().registerHandler( + Throwable.class, + ctx -> sendMessage.accept(ctx.context(), ctx.exception().getMessage()) + ).registerHandler( + CommandExecutionException.class, + ctx -> sendMessage.accept(ctx.context(), "Invalid Command Argument: " + ctx.exception().getCause().getMessage()) + ).registerHandler( + NoSuchCommandException.class, + ctx -> sendMessage.accept(ctx.context(), "Unknown command") + ).registerHandler( + NoPermissionException.class, + ctx -> sendMessage.accept(ctx.context(), "Insufficient permissions") + ).registerHandler(InvalidCommandSenderException.class, + ctx -> sendMessage.accept(ctx.context(), ctx.exception().getMessage()) + ).registerHandler(InvalidSyntaxException.class, + ctx -> sendMessage.accept(ctx.context(), + "Invalid Command Syntax. Correct command syntax is: /" + ctx.exception().correctSyntax())); + } +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDACommandFactory.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDACommandFactory.java new file mode 100644 index 0000000..cd8fac0 --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDACommandFactory.java @@ -0,0 +1,42 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import java.util.Collection; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.slash.CommandScope; + +@API(status = API.Status.STABLE, since = "1.0.0") +public interface JDACommandFactory { + + /** + * Creates the JDA commands. + * + * @param scope current scope + * @return created commands + */ + @NonNull Collection<@NonNull CommandData> createCommands(@NonNull CommandScope scope); +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAInteraction.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAInteraction.java new file mode 100644 index 0000000..1f7d25d --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAInteraction.java @@ -0,0 +1,106 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.immutables.value.Value; +import org.incendo.cloud.discord.immutables.StagedImmutableBuilder; + +@StagedImmutableBuilder +@Value.Immutable +@API(status = API.Status.STABLE, since = "1.0.0") +public interface JDAInteraction { + + /** + * Creates a new builder. + * + * @return the builder + */ + static ImmutableJDAInteraction.@NonNull UserBuildStage builder() { + return ImmutableJDAInteraction.builder(); + } + + /** + * Returns the user that triggered the interaction. + * + * @return the user + */ + @NonNull User user(); + + /** + * Returns the guild that triggered the interaction, if the interaction took place in a guild. + * + * @return the guild, or {@code null} + */ + @Nullable Guild guild(); + + /** + * Returns the interaction event that triggered the command, if relevant. + * + * @return the interaction event, or {@code null} + */ + @Nullable GenericCommandInteractionEvent interactionEvent(); + + /** + * Returns the reply callback, if relevant. + * + * @return the reply callback, or {@code null} + */ + @Nullable IReplyCallback replyCallback(); + + + /** + * Maps between {@link JDAInteraction} and {@link C}. + * + * @param command sender type + * @since 1.0.0 + */ + @FunctionalInterface + @API(status = API.Status.STABLE, since = "1.0.0") + interface InteractionMapper { + + /** + * Returns a mapper that maps the interaction to itself. + * + * @return identity mapper + */ + static @NonNull InteractionMapper identity() { + return interaction -> interaction; + } + + /** + * Maps the interaction to the custom sender. + * + * @param interaction interaction to map + * @return the mapped sender + */ + @NonNull C map(@NonNull JDAInteraction interaction); + } +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/ReplyCommandPostprocessor.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/ReplyCommandPostprocessor.java new file mode 100644 index 0000000..018de1c --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/ReplyCommandPostprocessor.java @@ -0,0 +1,62 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import cloud.commandframework.execution.postprocessor.CommandPostprocessingContext; +import cloud.commandframework.execution.postprocessor.CommandPostprocessor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.slash.DiscordSetting; + +final class ReplyCommandPostprocessor implements CommandPostprocessor { + + private final JDA5CommandManager commandManager; + + ReplyCommandPostprocessor(final @NonNull JDA5CommandManager commandManager) { + this.commandManager = commandManager; + } + + @Override + public void accept(final @NonNull CommandPostprocessingContext context) { + final JDAInteraction interaction = context.commandContext().get(JDA5CommandManager.CONTEXT_JDA_INTERACTION); + + final ReplySetting fallbackSetting; + if (this.commandManager.discordSettings().get(DiscordSetting.FORCE_DEFER_NON_EPHEMERAL)) { + fallbackSetting = ReplySetting.defer(false); + } else if (this.commandManager.discordSettings().get(DiscordSetting.FORCE_DEFER_EPHEMERAL)) { + fallbackSetting = ReplySetting.defer(true); + } else { + fallbackSetting = ReplySetting.doNotDefer(); + } + + final ReplySetting replySetting = context.command().commandMeta().getOrDefault( + JDA5CommandManager.META_REPLY_SETTING, + fallbackSetting + ); + if (replySetting.defer()) { + interaction.replyCallback().deferReply(replySetting.ephemeral()).queue(); + } + // This way we can keep track of whether we deferred or not. + context.commandContext().store(JDA5CommandManager.META_REPLY_SETTING, replySetting); + } +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/ReplySetting.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/ReplySetting.java new file mode 100644 index 0000000..acb9e5a --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/ReplySetting.java @@ -0,0 +1,76 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import cloud.commandframework.Command; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; +import org.incendo.cloud.discord.immutables.ImmutableImpl; + +@ImmutableImpl +@Value.Immutable +@API(status = API.Status.STABLE, since = "1.0.0") +public interface ReplySetting extends Command.Builder.Applicable { + + /** + * Returns a setting that does not defer the reply. + * + * @param command sender type + * @return the setting + */ + static @NonNull ReplySetting doNotDefer() { + return ReplySettingImpl.of(false, false); + } + + /** + * Returns a setting that defers the reply. + * + * @param command sender type + * @param ephemeral whether the reply is ephemeral + * @return the setting + */ + static @NonNull ReplySetting defer(final boolean ephemeral) { + return ReplySettingImpl.of(true, ephemeral); + } + + /** + * Whether to defer the reply. + * + * @return whether to defer + */ + boolean defer(); + + /** + * Whether the reply is ephemeral. + * + * @return whether the reply is ephemeral. + */ + boolean ephemeral(); + + @Override + default Command.@NonNull Builder applyToCommandBuilder(Command.@NonNull Builder builder) { + return builder.meta(JDA5CommandManager.META_REPLY_SETTING, this); + } +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/StandardJDACommandFactory.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/StandardJDACommandFactory.java new file mode 100644 index 0000000..23fdb7b --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/StandardJDACommandFactory.java @@ -0,0 +1,168 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import cloud.commandframework.CommandTree; +import cloud.commandframework.internal.CommandNode; +import cloud.commandframework.permission.Permission; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.slash.CommandScope; +import org.incendo.cloud.discord.slash.DiscordCommand; +import org.incendo.cloud.discord.slash.DiscordCommandFactory; +import org.incendo.cloud.discord.slash.DiscordOption; +import org.incendo.cloud.discord.slash.DiscordOptionChoice; +import org.incendo.cloud.discord.slash.DiscordOptionType; +import org.incendo.cloud.discord.slash.DiscordPermission; +import org.incendo.cloud.discord.slash.NodeProcessor; +import org.incendo.cloud.discord.slash.OptionRegistry; +import org.incendo.cloud.discord.slash.StandardDiscordCommandFactory; +import org.incendo.cloud.discord.slash.StandardOptionRegistry; + +public class StandardJDACommandFactory implements JDACommandFactory { + + private final CommandTree commandTree; + private final DiscordCommandFactory discordCommandFactory; + private final NodeProcessor nodeProcessor; + + /** + * Creates a new command factory. + * + * @param commandTree command tree to retrieve commands from + */ + public StandardJDACommandFactory(final @NonNull CommandTree commandTree) { + this.commandTree = commandTree; + + final OptionRegistry optionRegistry = new StandardOptionRegistry<>(); + this.discordCommandFactory = new StandardDiscordCommandFactory<>(optionRegistry); + + this.nodeProcessor = new NodeProcessor<>(this.commandTree); + } + + @Override + @SuppressWarnings("unchecked") + public @NonNull Collection<@NonNull CommandData> createCommands(final @NonNull CommandScope scope) { + this.nodeProcessor.prepareTree(); + + final List commands = new ArrayList<>(); + for (final CommandNode rootNode : this.commandTree.rootNodes()) { + final CommandScope rootScope = (CommandScope) rootNode.nodeMeta().get(NodeProcessor.NODE_META_SCOPE); + if (!rootScope.overlaps(scope)) { + System.out.println("Skipping creating of command " + rootNode.component().name() + " for scope " + scope); + continue; + } + + final DiscordCommand command = this.discordCommandFactory.create(rootNode); + SlashCommandData data = Commands.slash(command.name(), command.description()); + for (final DiscordOption option : command.options()) { + if (option instanceof DiscordOption.SubCommand) { + if (option.type().equals(DiscordOptionType.SUB_COMMAND)) { + data.addSubcommands(this.createSubCommand((DiscordOption.SubCommand) option)); + } else { + data.addSubcommandGroups(this.createSubCommandGroup((DiscordOption.SubCommand) option)); + } + } else { + data.addOptions(this.createOption((DiscordOption.Variable) option)); + } + } + + final Permission permission = (Permission) rootNode.nodeMeta().get(CommandNode.META_KEY_PERMISSION); + if (permission instanceof DiscordPermission) { + data.setDefaultPermissions(DefaultMemberPermissions.enabledFor(((DiscordPermission) permission).permission())); + } + + commands.add(data); + } + return commands; + } + + private @NonNull SubcommandData createSubCommand(final DiscordOption.@NonNull SubCommand option) { + SubcommandData subcommandData = new SubcommandData(option.name(), option.description()); + for (final DiscordOption child : option.options()) { + if (child instanceof DiscordOption.SubCommand) { + throw new IllegalArgumentException( + "Cannot add subcommand " + child.name() + " as a child of subcommand " + option.name() + ); + } + final OptionData childOption = this.createOption((DiscordOption.Variable) child); + subcommandData.addOptions(childOption); + } + return subcommandData; + } + + private @NonNull SubcommandGroupData createSubCommandGroup(final DiscordOption.@NonNull SubCommand option) { + SubcommandGroupData subcommandGroupData = new SubcommandGroupData(option.name(), option.description()); + for (final DiscordOption child : option.options()) { + if (child instanceof DiscordOption.Variable) { + throw new IllegalArgumentException( + "Cannot add variable option " + child.name() + " as child of group " + option.name() + ); + } + subcommandGroupData = subcommandGroupData.addSubcommands(this.createSubCommand((DiscordOption.SubCommand) child)); + } + return subcommandGroupData; + } + + private @NonNull OptionData createOption(final DiscordOption.@NonNull Variable option) { + OptionData optionData = new OptionData( + OptionType.fromKey(option.type().value()), + option.name(), + option.description() + ).setRequired(option.required()); + if (option.range() != null) { + optionData = optionData.setMinValue(option.range().min().longValue()).setMaxValue(option.range().max().longValue()); + } + if (option.autocomplete()) { + optionData = optionData.setAutoComplete(true); + } else { + optionData = optionData.addChoices(this.createChoices(option.choices())); + } + return optionData; + } + + private @NonNull Collection createChoices( + final @NonNull Collection<@NonNull DiscordOptionChoice> choices + ) { + return choices.stream().map(choice -> { + if (choice.value() instanceof Integer) { + return new Command.Choice(choice.name(), (int) choice.value()); + } else if (choice.value() instanceof Double) { + return new Command.Choice(choice.name(), (double) choice.value()); + } + return new Command.Choice(choice.name(), choice.value().toString()); + }).collect(Collectors.toList()); + } +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/package-info.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/package-info.java new file mode 100644 index 0000000..9aeb2a5 --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/package-info.java @@ -0,0 +1,4 @@ +/** + * Cloud implementation for JDA5. + */ +package org.incendo.cloud.discord.jda5; diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..22cf1ac --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +group=org.incendo +version=1.0.0-SNAPSHOT +description=cloud-discord + +org.gradle.caching=true +org.gradle.parallel=true diff --git a/gradle/build-logic/build.gradle.kts b/gradle/build-logic/build.gradle.kts new file mode 100644 index 0000000..1ad12cc --- /dev/null +++ b/gradle/build-logic/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + `kotlin-dsl` + alias(libs.plugins.cloud.buildLogic.spotless) +} + +repositories { + gradlePluginPortal() +} + +dependencies { + implementation(libs.cloud.build.logic) + implementation(libs.gradleKotlinJvm) + + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) +} + +cloudSpotless { + licenseHeaderFile.convention(null as RegularFile?) + ktlintVersion = libs.versions.ktlint +} diff --git a/gradle/build-logic/settings.gradle.kts b/gradle/build-logic/settings.gradle.kts new file mode 100644 index 0000000..6071c27 --- /dev/null +++ b/gradle/build-logic/settings.gradle.kts @@ -0,0 +1,15 @@ +rootProject.name = "build-logic" + +pluginManagement { + repositories { + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../libs.versions.toml")) + } + } +} diff --git a/gradle/build-logic/src/main/kotlin/cloud-discord.base-conventions.gradle.kts b/gradle/build-logic/src/main/kotlin/cloud-discord.base-conventions.gradle.kts new file mode 100644 index 0000000..ec1c648 --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/cloud-discord.base-conventions.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("org.incendo.cloud-build-logic") + id("org.incendo.cloud-build-logic.spotless") +} + +indra { + javaVersions { + minimumToolchain(8) + target(8) + testWith().set(setOf(8, 11, 17)) + } + checkstyle().set(libs.versions.checkstyle) +} + +cloudSpotless { + ktlintVersion = libs.versions.ktlint +} + +spotless { + java { + importOrderFile(rootProject.file(".spotless/cloud-discord.importorder")) + } +} + +// Common dependencies. +dependencies { + // external + compileOnly(libs.immutables) + annotationProcessor(libs.immutables) + + // test dependencies + testImplementation(libs.jupiter.engine) + testImplementation(libs.jupiter.params) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.jupiter) + testImplementation(libs.truth) + testImplementation(libs.truth.java8) +} diff --git a/gradle/build-logic/src/main/kotlin/cloud-discord.publishing-conventions.gradle.kts b/gradle/build-logic/src/main/kotlin/cloud-discord.publishing-conventions.gradle.kts new file mode 100644 index 0000000..0151a69 --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/cloud-discord.publishing-conventions.gradle.kts @@ -0,0 +1,20 @@ +import org.incendo.cloudbuildlogic.city + +plugins { + id("org.incendo.cloud-build-logic.publishing") +} + +indra { + github("Incendo", "cloud-discord") { + ci(true) + } + mitLicense() + + configurePublications { + pom { + developers { + city() + } + } + } +} diff --git a/gradle/build-logic/src/main/kotlin/extensions.kt b/gradle/build-logic/src/main/kotlin/extensions.kt new file mode 100644 index 0000000..12ae20b --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/extensions.kt @@ -0,0 +1,6 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.kotlin.dsl.the + +val Project.libs: LibrariesForLibs + get() = the() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..06ac429 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,45 @@ +[plugins] +cloud-buildLogic-spotless = { id = "org.incendo.cloud-build-logic.spotless", version.ref = "cloud-build-logic" } +cloud-buildLogic-rootProject-publishing = { id = "org.incendo.cloud-build-logic.publishing.root-project", version.ref = "cloud-build-logic" } +cloud-buildLogic-rootProject-spotless = { id = "org.incendo.cloud-build-logic.spotless.root-project", version.ref = "cloud-build-logic" } + +[versions] +cloud-build-logic = "0.0.3" +ktlint = "1.0.1" +checkstyle = "10.12.5" +kotlin = "1.9.20" + +# Cloud +cloud = "2.0.0-SNAPSHOT" + +# External +immutables = "2.10.0" +jda = "5.0.0-beta.19" + +# Test +jupiterEngine = "5.10.1" +mockitoCore = "4.11.0" +mockitoJupiter = "4.11.0" +truth = "1.1.4" + +[libraries] +cloud-build-logic = { module = "org.incendo:cloud-build-logic", version.ref = "cloud-build-logic" } +gradleKotlinJvm = { group = "org.jetbrains.kotlin.jvm", name = "org.jetbrains.kotlin.jvm.gradle.plugin", version.ref = "kotlin" } + +# Cloud +cloud-core = { group = "cloud.commandframework", name = "cloud-core", version.ref = "cloud" } +cloud-annotations = { group = "cloud.commandframework", name = "cloud-annotations", version.ref = "cloud" } + +# External +immutables = { group = "org.immutables", name = "value", version.ref = "immutables" } +jda = { group = "net.dv8tion", name = "JDA", version.ref = "jda" } + +# Test +jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "jupiterEngine" } +jupiter-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "jupiterEngine" } +mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockitoCore" } +mockito-jupiter = { group = "org.mockito", name = "mockito-junit-jupiter", version.ref = "mockitoJupiter" } +truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } +truth-java8 = { group = "com.google.truth.extensions", name = "truth-java8-extension", version.ref = "truth" } + +[bundles] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f92b2bf --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jan 13 17:52:02 CET 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..d91400c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,31 @@ +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +pluginManagement { + repositories { + gradlePluginPortal() + } + includeBuild("gradle/build-logic") +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenLocal() + mavenCentral() + maven("https://oss.sonatype.org/content/repositories/snapshots/") { + name = "sonatypeOssSnapshots" + mavenContent { + snapshotsOnly() + } + } + } +} + +rootProject.name = "cloud-discord" + +include(":cloud-discord-common") +include(":cloud-jda5")