diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2842f87 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,376 @@ +root = true + +[*] +charset = utf-8 +end_of_line = crlf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +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_visual_guides = none +ij_wrap_on_typing = false + +[*.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 = true +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 = true +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 = normal +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 = 0 +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_builder_methods = none +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +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 = 5 +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 = never +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 = false +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 = true +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_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +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 = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = never +ij_java_imports_layout = *,|,javax.**,java.**,|,$* +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 = 2 +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_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = false +ij_java_keep_first_column_comment = false +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = false +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 = true +ij_java_keep_simple_lambdas_in_one_line = true +ij_java_keep_simple_methods_in_one_line = true +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_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +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 = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 3 +ij_java_new_line_after_lparen_in_record_header = false +ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +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 = true +ij_java_prefer_parameters_wrap = false +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_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +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_record_header = 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 = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +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 = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = true + +[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.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 = true +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal +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 = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +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 = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,strikt.api.**,strikt.assertions.**,kotlinx.android.synthetic.**,io.ktor.* +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 = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] +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 + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0454bb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +### Gradle template +.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 + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### JetBrains template +# 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 + +# 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 + +# 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 + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..2f86a17 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..0f7bc51 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..659bf43 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..94afc4c --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + mongo + true + com.dbschema.MongoJdbcDriver + mongodb://localhost:27017 + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..9079745 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml new file mode 100644 index 0000000..7d04a74 --- /dev/null +++ b/.idea/inspectionProfiles/ktlint.xml @@ -0,0 +1,7 @@ + + + + diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..64580d1 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..8c93757 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries-with-intellij-classes.xml b/.idea/libraries-with-intellij-classes.xml new file mode 100644 index 0000000..9fa3156 --- /dev/null +++ b/.idea/libraries-with-intellij-classes.xml @@ -0,0 +1,65 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2e420cc --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..78cface --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..26009d4 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Malware Check Bot +This Discord Bot scans message for links and submits them to the [Google Safebrowsing API](https://developers.google.com/safe-browsing/) and warns you when something malicious was found. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..f87623c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + kotlin("jvm") version "1.5.10" + kotlin("plugin.serialization") version "1.5.10" + id("org.jlleitschuh.gradle.ktlint") version "10.1.0" + application +} + +group = "de.nycode" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://oss.sonatype.org/content/repositories/snapshots") + maven("https://schlaubi.jfrog.io/artifactory/envconf/") { + content { + includeModuleByRegex("dev.schlaubi", "envconf(?:-jvm)?") + } + } +} + +dependencies { + implementation("dev.kord", "kord-core", "0.7.x-SNAPSHOT") + + // Logging + implementation("io.github.microutils", "kotlin-logging", "2.0.8") + implementation("ch.qos.logback", "logback-classic", "1.3.0-alpha5") + implementation("io.sentry", "sentry", "4.3.0") + implementation("io.sentry", "sentry-logback", "4.3.0") + + // Configuration + implementation("dev.schlaubi", "envconf", "1.1") + + // MongoDB + implementation("org.litote.kmongo", "kmongo-coroutine-core", "4.2.7") + implementation("org.litote.kmongo", "kmongo-serialization-mapping", "4.2.7") + + implementation(platform("io.ktor:ktor-bom:1.6.0")) + implementation("io.ktor", "ktor-client-core") + implementation("io.ktor", "ktor-client-serialization") + implementation("io.ktor", "ktor-client-okhttp") + + implementation("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.2.1") +} + +tasks { + withType { + kotlinOptions { + jvmTarget = "16" + freeCompilerArgs = + listOf( + "-Xopt-in=dev.kord.common.annotation.KordPreview", + "-Xopt-in=kotlin.RequiresOptIn", + "-Xopt-in=kotlin.ExperimentalStdlibApi" + ) + } + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..b4cbbc4 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,13 @@ +services: + mongo: + image: mongo + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: root + ports: + - 27017:27017 + volumes: + - mongo_data:/data/db + +volumes: + mongo_data: diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c 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..29e4134 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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 "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /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..915ef00 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "MalwareCheckBot" diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/Launcher.kt b/src/main/kotlin/de/nycode/malwarecheckbot/Launcher.kt new file mode 100644 index 0000000..ab8b0c1 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/Launcher.kt @@ -0,0 +1,37 @@ +package de.nycode.malwarecheckbot + +import ch.qos.logback.classic.Logger +import de.nycode.malwarecheckbot.config.Config +import de.nycode.malwarecheckbot.core.MalwareCheckBotImpl +import io.sentry.Sentry +import io.sentry.SentryOptions +import mu.KotlinLogging +import org.slf4j.LoggerFactory + +private val logger = KotlinLogging.logger {} + +suspend fun main() { + initializeLogging() + initializeSentry() + MalwareCheckBotImpl().start() +} + +private fun initializeLogging() { + val rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger + rootLogger.level = Config.LOG_LEVEL + + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + logger.error(throwable) { "Got unhandled error on $thread" } + } +} + +private fun initializeSentry() { + val configure: (SentryOptions) -> Unit = + if (Config.ENVIRONMENT.useSentry) { + { it.dsn = Config.SENTRY_TOKEN; } + } else { + { it.dsn = "" } + } + + Sentry.init(configure) +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/config/Config.kt b/src/main/kotlin/de/nycode/malwarecheckbot/config/Config.kt new file mode 100644 index 0000000..de239c8 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/config/Config.kt @@ -0,0 +1,23 @@ +package de.nycode.malwarecheckbot.config + +import ch.qos.logback.classic.Level +import com.mongodb.ConnectionString +import dev.schlaubi.envconf.environment +import dev.schlaubi.envconf.getEnv + +object Config { + + val DISCORD_TOKEN by environment + + val ENVIRONMENT by getEnv(default = Environment.PRODUCTION) { Environment.valueOf(it) } + + val SENTRY_TOKEN by getEnv(default = "") + + val LOG_LEVEL by getEnv(default = Level.ERROR) { Level.valueOf(it) } + + val GCP_SAFE_BROWSING_API_KEY by environment + + val MONGO_CONNECTION_STRING: ConnectionString by getEnv { ConnectionString(it) } + + val MONGO_DATABASE by getEnv(default = "malware-check-bot") +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/config/Environment.kt b/src/main/kotlin/de/nycode/malwarecheckbot/config/Environment.kt new file mode 100644 index 0000000..ab7bb2e --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/config/Environment.kt @@ -0,0 +1,8 @@ +package de.nycode.malwarecheckbot.config + +enum class Environment(val useSentry: Boolean) { + + PRODUCTION(true), + + DEVELOPMENT(false) +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/core/MalwareCheckBot.kt b/src/main/kotlin/de/nycode/malwarecheckbot/core/MalwareCheckBot.kt new file mode 100644 index 0000000..dd0da23 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/core/MalwareCheckBot.kt @@ -0,0 +1,13 @@ +package de.nycode.malwarecheckbot.core + +import de.nycode.malwarecheckbot.storage.GuildInformation +import dev.kord.core.Kord +import kotlinx.coroutines.CoroutineScope +import org.litote.kmongo.coroutine.CoroutineCollection + +interface MalwareCheckBot : CoroutineScope { + val kord: Kord + val repositories: Repositories +} + +class Repositories(val guilds: CoroutineCollection) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/core/MalwareCheckBotImpl.kt b/src/main/kotlin/de/nycode/malwarecheckbot/core/MalwareCheckBotImpl.kt new file mode 100644 index 0000000..529085e --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/core/MalwareCheckBotImpl.kt @@ -0,0 +1,40 @@ +package de.nycode.malwarecheckbot.core + +import de.nycode.malwarecheckbot.config.Config +import de.nycode.malwarecheckbot.listener.registerChatListener +import de.nycode.malwarecheckbot.listener.registerJoinListener +import de.nycode.malwarecheckbot.storage.GuildInformation +import de.nycode.malwarecheckbot.utils.createMongoClient +import dev.kord.core.Kord +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob + +internal class MalwareCheckBotImpl : MalwareCheckBot { + + override val coroutineContext = Dispatchers.IO + SupervisorJob() + + override lateinit var kord: Kord + + override lateinit var repositories: Repositories + + suspend fun start() { + kord = Kord(Config.DISCORD_TOKEN) + + repositories = initializeDatabase() + + registerJoinListener() + registerChatListener() + + kord.login() + } + + private fun initializeDatabase(): Repositories { + val client = createMongoClient(Config.MONGO_CONNECTION_STRING) + val database = client.getDatabase(Config.MONGO_DATABASE) + + val guildCollection = database.getCollection() + + return Repositories(guildCollection) + } + +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/MalwareDetector.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/MalwareDetector.kt new file mode 100644 index 0000000..c73d445 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/MalwareDetector.kt @@ -0,0 +1,58 @@ +package de.nycode.malwarecheckbot.filtering + +import de.nycode.malwarecheckbot.config.Config +import de.nycode.malwarecheckbot.filtering.safebrowsing.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.features.json.JsonFeature +import io.ktor.client.features.json.serializer.KotlinxSerializer +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.http.ContentType +import io.ktor.http.contentType + +private const val GOOGLE_SAFE_BROWSING_BASE_URL = "https://safebrowsing.googleapis.com" + +class MalwareDetector { + + private val client = HttpClient(OkHttp) { + install(JsonFeature) { + serializer = KotlinxSerializer() + } + } + + // https://regex101.com/r/bpCMoW/1 + private val urlRegex = + "(?:https?://.)?(www\\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,6}\\b[-a-zA-Z0-9@:%_+.~#?&//=]*".toRegex() + + fun detectUrls(text: String): List { + return urlRegex.findAll(text).map(MatchResult::value).toList() + } + + suspend fun checkForMalware(urls: List) = client.post(GOOGLE_SAFE_BROWSING_BASE_URL) { + parameter("key", Config.GCP_SAFE_BROWSING_API_KEY) + contentType(ContentType.Application.Json) + url { + path("v4", "threatMatches:find") + } + body = createSafeBrowsingRequestBody { + client { + id = "malware-check-discord-bot-nycode" + version = "1.0.0-SNAPSHOT" + } + threatInfo { + platformTypes = listOf(PlatformType.ANY_PLATFORM) + threatTypes = listOf( + ThreatType.MALWARE, + ThreatType.THREAT_TYPE_UNSPECIFIED, + ThreatType.POTENTIALLY_HARMFUL_APPLICATION, + ThreatType.SOCIAL_ENGINEERING, + ThreatType.UNWANTED_SOFTWARE + ) + threatEntryTypes = listOf(ThreatEntryType.URL) + threatEntries = urls.map { ThreatEntry(url = it) } + } + } + } + +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ClientInfo.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ClientInfo.kt new file mode 100644 index 0000000..281906a --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ClientInfo.kt @@ -0,0 +1,9 @@ +package de.nycode.malwarecheckbot.filtering.safebrowsing + +import kotlinx.serialization.Serializable + +@Serializable +data class ClientInfo( + val clientId: String, + val clientVersion: String +) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/PlatformType.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/PlatformType.kt new file mode 100644 index 0000000..1e95196 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/PlatformType.kt @@ -0,0 +1,13 @@ +package de.nycode.malwarecheckbot.filtering.safebrowsing + +enum class PlatformType { + PLATFORM_TYPE_UNSPECIFIED, + WINDOWS, + LINUX, + ANDROID, + OSX, + IOS, + ANY_PLATFORM, + ALL_PLATFORMS, + CHROME +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/SafeBrowsingRequest.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/SafeBrowsingRequest.kt new file mode 100644 index 0000000..6efb756 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/SafeBrowsingRequest.kt @@ -0,0 +1,48 @@ +package de.nycode.malwarecheckbot.filtering.safebrowsing + +import io.ktor.client.request.HttpRequestBuilder +import kotlinx.serialization.Serializable + +@Serializable +data class SafeBrowsingRequest( + val client: ClientInfo, + val threatInfo: ThreatInfo +) + +fun HttpRequestBuilder.createSafeBrowsingRequestBody(builder: SafeBrowsingRequestBuilder.() -> Unit): SafeBrowsingRequest { + val safeBrowsingRequestBuilder = SafeBrowsingRequestBuilder().apply(builder) + return safeBrowsingRequestBuilder.toRequest() +} + +data class SafeBrowsingRequestBuilder( + var client: ClientInfo = ClientInfo("", ""), + var threatInfo: ThreatInfo = ThreatInfo(emptyList(), emptyList(), emptyList(), emptyList()) +) { + fun client(builder: SafeBrowsingClientInfoBuilder.() -> Unit) { + val clientInfoBuilder = SafeBrowsingClientInfoBuilder("", "").apply(builder) + client = clientInfoBuilder.toClientInfo() + } + + fun threatInfo(builder: ThreatInfoBuilder.() -> Unit) { + val threatInfoBuilder = ThreatInfoBuilder(emptyList(), emptyList(), emptyList(), emptyList()).apply(builder) + threatInfo = threatInfoBuilder.toThreatInfo() + } + + fun toRequest() = SafeBrowsingRequest(client, threatInfo) +} + +data class ThreatInfoBuilder( + var threatTypes: List, + var platformTypes: List, + var threatEntryTypes: List, + var threatEntries: List +) { + fun toThreatInfo() = ThreatInfo(threatTypes, platformTypes, threatEntryTypes, threatEntries) +} + +data class SafeBrowsingClientInfoBuilder( + var id: String, + var version: String +) { + fun toClientInfo() = ClientInfo(id, version) +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/SafeBrowsingResponse.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/SafeBrowsingResponse.kt new file mode 100644 index 0000000..8e19833 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/SafeBrowsingResponse.kt @@ -0,0 +1,18 @@ +package de.nycode.malwarecheckbot.filtering.safebrowsing + +import de.nycode.malwarecheckbot.serialization.MetadataEntryMapSerializer +import kotlinx.serialization.Serializable + +@Serializable +data class SafeBrowsingResponse(val matches: List = emptyList()) + +@Serializable +data class ThreatMatch( + val threatType: ThreatType, + val platformType: PlatformType, + val threatEntryType: ThreatEntryType, + val threat: ThreatEntry, + @Serializable(with = MetadataEntryMapSerializer::class) + val threatEntryMetadata: Map = emptyMap(), + val cacheDuration: String +) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatEntry.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatEntry.kt new file mode 100644 index 0000000..c1c1db7 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatEntry.kt @@ -0,0 +1,10 @@ +package de.nycode.malwarecheckbot.filtering.safebrowsing + +import kotlinx.serialization.Serializable + +@Serializable +data class ThreatEntry( + val hash: String? = null, + val url: String? = null, + val digest: String? = null +) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatEntryType.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatEntryType.kt new file mode 100644 index 0000000..ae220fb --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatEntryType.kt @@ -0,0 +1,7 @@ +package de.nycode.malwarecheckbot.filtering.safebrowsing + +enum class ThreatEntryType { + THREAT_ENTRY_TYPE_UNSPECIFIED, + URL, + EXECUTABLE +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatInfo.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatInfo.kt new file mode 100644 index 0000000..86ef887 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatInfo.kt @@ -0,0 +1,11 @@ +package de.nycode.malwarecheckbot.filtering.safebrowsing + +import kotlinx.serialization.Serializable + +@Serializable +data class ThreatInfo( + val threatTypes: List, + val platformTypes: List, + val threatEntryTypes: List, + val threatEntries: List +) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatType.kt b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatType.kt new file mode 100644 index 0000000..1aae444 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/filtering/safebrowsing/ThreatType.kt @@ -0,0 +1,12 @@ +package de.nycode.malwarecheckbot.filtering.safebrowsing + +/** + * Represents the [ThreatType](https://developers.google.com/safe-browsing/v4/reference/rest/v4/ThreatType) from the Google Safe Browsing API (v4). + */ +enum class ThreatType { + THREAT_TYPE_UNSPECIFIED, + MALWARE, + SOCIAL_ENGINEERING, + UNWANTED_SOFTWARE, + POTENTIALLY_HARMFUL_APPLICATION +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/listener/MalwareCheckingListener.kt b/src/main/kotlin/de/nycode/malwarecheckbot/listener/MalwareCheckingListener.kt new file mode 100644 index 0000000..b1d3b15 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/listener/MalwareCheckingListener.kt @@ -0,0 +1,28 @@ +package de.nycode.malwarecheckbot.listener + +import de.nycode.malwarecheckbot.core.MalwareCheckBot +import de.nycode.malwarecheckbot.filtering.MalwareDetector +import dev.kord.core.event.message.MessageCreateEvent +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +private val malwareDetector = MalwareDetector() + +fun MalwareCheckBot.registerChatListener() = kord.events + .filterIsInstance() + .filter { it.member?.isBot != true } + .onEach { event -> + val content = event.message.content + val urls = malwareDetector.detectUrls(content) + if (urls.isEmpty()) { + return@onEach + } + + val response = malwareDetector.checkForMalware(urls) + + event.message.channel.createMessage(response.toString()) + + } + .launchIn(this) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/listener/ServerJoinListener.kt b/src/main/kotlin/de/nycode/malwarecheckbot/listener/ServerJoinListener.kt new file mode 100644 index 0000000..8c48c70 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/listener/ServerJoinListener.kt @@ -0,0 +1,33 @@ +package de.nycode.malwarecheckbot.listener + +import de.nycode.malwarecheckbot.core.MalwareCheckBot +import de.nycode.malwarecheckbot.storage.GuildInformation +import de.nycode.malwarecheckbot.utils.Embeds +import de.nycode.malwarecheckbot.utils.Embeds.createEmbed +import dev.kord.core.event.guild.GuildCreateEvent +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +fun MalwareCheckBot.registerJoinListener() = + kord.events + .filterIsInstance() + .filter { repositories.guilds.findOneById(it.guild.id) == null } + .onEach { event -> + val guild = event.guild + guild.systemChannel?.createEmbed(Embeds.info("Beep Boop! :robot:") { + description = "Thank you for adding me to your server. \n" + + "I will take care of your users from now on and protect them from malware links.\n\n" + + ":warning: **Notice:** I'm not a replacement for moderation. I'm not able to detect **every** malicious link." + }) + + repositories.guilds.save( + GuildInformation( + guild.id, + deleteMaliciousMessages = true, + showLinkChecking = true + ) + ) + } + .launchIn(this) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/serialization/MetadataEntryMapSerializer.kt b/src/main/kotlin/de/nycode/malwarecheckbot/serialization/MetadataEntryMapSerializer.kt new file mode 100644 index 0000000..1962044 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/serialization/MetadataEntryMapSerializer.kt @@ -0,0 +1,27 @@ +package de.nycode.malwarecheckbot.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object MetadataEntryMapSerializer : KSerializer> { + override val descriptor: SerialDescriptor = ThreatEntryMetadataSurrogate.serializer().descriptor + + override fun deserialize(decoder: Decoder): Map { + val surrogate = decoder.decodeSerializableValue(ThreatEntryMetadataSurrogate.serializer()) + return surrogate.entries.toMap() + } + + override fun serialize(encoder: Encoder, value: Map) { + val surrogate = ThreatEntryMetadataSurrogate(value.toList()) + encoder.encodeSerializableValue(ThreatEntryMetadataSurrogate.serializer(), surrogate) + } + +} + +@Serializable +class ThreatEntryMetadataSurrogate( + val entries: List<@Serializable(with = MetadataPairSerializer::class) Pair> = emptyList() +) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/serialization/MetadataPairSerializer.kt b/src/main/kotlin/de/nycode/malwarecheckbot/serialization/MetadataPairSerializer.kt new file mode 100644 index 0000000..d9c68d3 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/serialization/MetadataPairSerializer.kt @@ -0,0 +1,29 @@ +package de.nycode.malwarecheckbot.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure + +object MetadataPairSerializer : KSerializer> { + override val descriptor = buildClassSerialDescriptor("Pair") { + element("key") + element("value") + } + + override fun deserialize(decoder: Decoder): Pair { + return decoder.decodeStructure(descriptor) { + decodeStringElement(descriptor, 0) to decodeStringElement(descriptor, 1) + } + } + + override fun serialize(encoder: Encoder, value: Pair) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.first) + encodeStringElement(descriptor, 1, value.second) + } + } +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/serialization/SnowflakeSerializer.kt b/src/main/kotlin/de/nycode/malwarecheckbot/serialization/SnowflakeSerializer.kt new file mode 100644 index 0000000..8f86836 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/serialization/SnowflakeSerializer.kt @@ -0,0 +1,22 @@ +package de.nycode.malwarecheckbot.serialization + +import dev.kord.common.entity.Snowflake +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * Temporary Solution as an alternative to the build in [Snowflake] Serializer until [#313](https://github.com/kordlib/kord/issues/313) gets fixed. + */ +object SnowflakeSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("Snowflake", PrimitiveKind.STRING) + override fun deserialize(decoder: Decoder): Snowflake { + return Snowflake(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: Snowflake) { + encoder.encodeString(value.asString) + } +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/storage/GuildInformation.kt b/src/main/kotlin/de/nycode/malwarecheckbot/storage/GuildInformation.kt new file mode 100644 index 0000000..ac5ae32 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/storage/GuildInformation.kt @@ -0,0 +1,13 @@ +package de.nycode.malwarecheckbot.storage + +import de.nycode.malwarecheckbot.serialization.SnowflakeSerializer +import dev.kord.common.entity.Snowflake +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GuildInformation( + @SerialName("_id") @Serializable(with = SnowflakeSerializer::class) val id: Snowflake, + val deleteMaliciousMessages: Boolean, + val showLinkChecking: Boolean +) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/utils/Colors.kt b/src/main/kotlin/de/nycode/malwarecheckbot/utils/Colors.kt new file mode 100644 index 0000000..c14532e --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/utils/Colors.kt @@ -0,0 +1,30 @@ +package de.nycode.malwarecheckbot.utils + +import dev.kord.common.Color + +/** + * Wrapper for [Discordapp.com/branding][https://discordapp.com/branding] colors and some other colors: + */ +@Suppress("KDocMissingDocumentation", "unused", "MagicNumber") +object Colors { + // Discord + val BLURLPLE: Color = Color(88, 101, 242) + val GREEN: Color = Color(87, 242, 135) + val YELLOW: Color = Color(254, 231, 92) + val FUCHSIA: Color = Color(235, 69, 158) + val RED: Color = Color(237, 66, 69) + val WHITE: Color = Color(255, 255, 255) + val BLACK: Color = Color(0, 0, 0) + + // Old Discord Branding Colors + val GREYPLE: Color = Color(153, 170, 181) + val DARK_BUT_NOT_BLACK: Color = Color(44, 47, 51) + val NOT_QUITE_BLACK: Color = Color(33, 39, 42) + + // Other colors + val LIGHT_RED: Color = Color(231, 76, 60) + val DARK_RED: Color = Color(192, 57, 43) + val LIGHT_GREEN: Color = Color(46, 204, 113) + val DARK_GREEN: Color = Color(39, 174, 96) + val BLUE: Color = Color(52, 152, 219) +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/utils/EmbedUtils.kt b/src/main/kotlin/de/nycode/malwarecheckbot/utils/EmbedUtils.kt new file mode 100644 index 0000000..ea92038 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/utils/EmbedUtils.kt @@ -0,0 +1,5 @@ +package de.nycode.malwarecheckbot.utils + +import dev.kord.rest.builder.message.EmbedBuilder + +fun embed(builder: EmbedBuilder.() -> Unit) = EmbedBuilder().apply(builder) diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/utils/Embeds.kt b/src/main/kotlin/de/nycode/malwarecheckbot/utils/Embeds.kt new file mode 100644 index 0000000..b274199 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/utils/Embeds.kt @@ -0,0 +1,118 @@ +package de.nycode.malwarecheckbot.utils + +import dev.kord.core.behavior.MessageBehavior +import dev.kord.core.behavior.channel.MessageChannelBehavior +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.edit +import dev.kord.core.entity.Message +import dev.kord.rest.builder.message.EmbedBuilder + +typealias EmbedCreator = EmbedBuilder.() -> Unit + +object Embeds { + /** + * Creates a info embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun info(title: String, description: String? = null, builder: EmbedCreator = {}): EmbedBuilder = + embed { + title(Emotes.INFO, title) + this.description = description + color = Colors.BLUE + }.apply(builder) + + /** + * Creates a success embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun success( + title: String, + description: String? = null, + builder: EmbedCreator = {} + ): EmbedBuilder = + embed { + title(Emotes.SUCCESS, title) + this.description = description + color = Colors.GREEN + }.apply(builder) + + /** + * Creates a error embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun error( + title: String, + description: String? = null, + builder: EmbedCreator = {} + ): EmbedBuilder = + embed { + title(Emotes.ERROR, title) + this.description = description + color = Colors.RED + }.apply(builder) + + /** + * Creates a warning embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun warn( + title: String, + description: String? = null, + builder: EmbedCreator = {} + ): EmbedBuilder = + embed { + title(Emotes.WARN, title) + this.description = description + color = Colors.YELLOW + }.apply(builder) + + /** + * Creates a loading embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun loading( + title: String, + description: String?, + builder: EmbedCreator = {} + ): EmbedBuilder = + embed { + title(Emotes.LOADING, title) + this.description = description + color = Colors.DARK_BUT_NOT_BLACK + }.apply(builder) + + private fun EmbedBuilder.title(emote: String, title: String) { + this.title = "$emote $title" + } + + /** + * Sends a new message in this channel containing the embed provided by [base] and applies [creator] to it + */ + suspend fun MessageChannelBehavior.createEmbed( + base: EmbedBuilder, + creator: suspend EmbedBuilder.() -> Unit = {} + ): Message { + return createMessage { + creator(base) + embed = base + } + } + + /** + * Sends a new message in this channel containing the embed provided by [base] and applies [creator] to it + */ + suspend fun MessageBehavior.editEmbed( + base: EmbedBuilder, + creator: suspend EmbedBuilder.() -> Unit = {} + ): Message { + return edit { + creator(base) + embed = base + } + } +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/utils/Emotes.kt b/src/main/kotlin/de/nycode/malwarecheckbot/utils/Emotes.kt new file mode 100644 index 0000000..ffbcf7f --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/utils/Emotes.kt @@ -0,0 +1,17 @@ +package de.nycode.malwarecheckbot.utils + +/** + * Useful collection of Discord emotes. + * + * + * Designed by [Rxsto#1337](https://rxsto.me) + * Bot needs to be on [https://discord.gg/8phqcej](https://discord.gg/8phqcej) + */ +@Suppress("KDocMissingDocumentation") +object Emotes { + const val LOADING: String = "" + const val ERROR: String = "<:error:535827110489620500>" + const val WARN: String = "<:warn:535832532365737987>" + const val INFO: String = "<:info:535828529573789696>" + const val SUCCESS: String = "<:success:535827110552666112>" +} diff --git a/src/main/kotlin/de/nycode/malwarecheckbot/utils/MongoUtils.kt b/src/main/kotlin/de/nycode/malwarecheckbot/utils/MongoUtils.kt new file mode 100644 index 0000000..6c994c4 --- /dev/null +++ b/src/main/kotlin/de/nycode/malwarecheckbot/utils/MongoUtils.kt @@ -0,0 +1,18 @@ +package de.nycode.malwarecheckbot.utils + +import com.mongodb.ConnectionString +import com.mongodb.MongoClientSettings +import org.bson.UuidRepresentation +import org.litote.kmongo.coroutine.coroutine +import org.litote.kmongo.reactivestreams.KMongo + +/** + * Create a new [com.mongodb.reactivestreams.client.MongoClient] with Standard uuid representation. + */ +fun createMongoClient(connectionString: ConnectionString) = KMongo.createClient( + MongoClientSettings + .builder() + .uuidRepresentation(UuidRepresentation.STANDARD) + .applyConnectionString(connectionString) + .build() +).coroutine diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..eb3f4fc --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,40 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + logs/bot.%d{yyyy-MM-dd}.%i.log + 100MB + 90 + + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %-40.40logger{39} : %msg%n + + + true + + + + 512 + + + + + + ERROR + + + + + + + + +