diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f9ff432 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ + +# IDE +.idea/ +.vscode/ +.gradle/ +.settings/ + +#build +/build/ +/bin/ +.metals/ +.bloop/ +.project +lcoal.properties + +# debug dir +/run/ diff --git a/.editorconfig b/.editorconfig index ee8e124..424d5bf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,3 @@ -root = true - [*] charset = utf-8 end_of_line = lf @@ -11,11 +9,50 @@ 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_formatter_tags_enabled = true ij_smart_tabs = false -ij_visual_guides = none +ij_visual_guides = ij_wrap_on_typing = false +[*.conf] +ij_smart_tabs = true +ij_hocon_keep_blank_lines_before_right_brace = 2 +ij_hocon_keep_indents_on_empty_lines = true +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_smart_tabs = true +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_block_comment_add_space = false +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 = true +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 = true +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 + +[*.feature] +indent_size = 2 +indent_style = space +ij_gherkin_keep_indents_on_empty_lines = false + [*.java] ij_java_align_consecutive_assignments = false ij_java_align_consecutive_variable_declarations = false @@ -25,6 +62,7 @@ ij_java_align_multiline_array_initializer_expression = false ij_java_align_multiline_assignment = true ij_java_align_multiline_binary_operation = false ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true ij_java_align_multiline_extends_list = false ij_java_align_multiline_for = false ij_java_align_multiline_method_parentheses = false @@ -38,6 +76,7 @@ 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 = true +ij_java_align_types_in_multi_catch = true ij_java_annotation_parameter_wrap = off ij_java_array_initializer_new_line_after_left_brace = true ij_java_array_initializer_right_brace_on_new_line = true @@ -64,7 +103,7 @@ ij_java_blank_lines_before_package = 0 ij_java_block_brace_style = end_of_line ij_java_block_comment_add_space = false ij_java_block_comment_at_first_column = true -ij_java_builder_methods = none +ij_java_builder_methods = 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 = normal @@ -74,8 +113,10 @@ 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_deconstruction_list_wrap = normal ij_java_do_not_indent_top_level_class_members = false ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = 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 @@ -96,18 +137,31 @@ ij_java_doc_param_description_on_new_line = false ij_java_doc_preserve_line_breaks = true ij_java_doc_use_throws_not_exception_tag = true ij_java_else_on_new_line = false +ij_java_entity_dd_prefix = ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = 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_li_suffix = ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = ij_java_entity_vo_suffix = VO ij_java_enum_constants_wrap = on_every_item ij_java_extends_keyword_wrap = normal ij_java_extends_list_wrap = off ij_java_field_annotation_wrap = normal +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = ij_java_finally_on_new_line = false ij_java_for_brace_force = if_multiline ij_java_for_statement_new_line_after_left_paren = true @@ -139,8 +193,15 @@ 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_add_space_on_reformat = false ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = ij_java_message_eb_suffix = Bean ij_java_method_annotation_wrap = split_into_lines ij_java_method_brace_style = end_of_line @@ -149,16 +210,22 @@ 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 = normal ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal ij_java_names_count_to_use_import_on_demand = 3 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true 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 = normal +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = 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_annotations = ij_java_repeat_synchronized = true ij_java_replace_instanceof_and_cast = false ij_java_replace_null_check = true @@ -166,13 +233,26 @@ ij_java_replace_sum_lambda_with_method_ref = true ij_java_resource_list_new_line_after_left_paren = true ij_java_resource_list_right_paren_on_new_line = true ij_java_resource_list_wrap = normal +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true ij_java_rparen_on_new_line_in_record_header = false +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = 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_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = ij_java_session_si_suffix = Service ij_java_space_after_closing_angle_bracket_in_type_argument = false ij_java_space_after_colon = true @@ -191,6 +271,7 @@ 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_deconstruction_list = false ij_java_space_before_do_left_brace = true ij_java_space_before_else_keyword = true ij_java_space_before_else_left_brace = true @@ -221,6 +302,7 @@ 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_annotation_eq = true ij_java_spaces_around_assignment_operators = true ij_java_spaces_around_bitwise_operators = true ij_java_spaces_around_equality_operators = true @@ -239,6 +321,7 @@ 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_deconstruction_list = false ij_java_spaces_within_for_parentheses = false ij_java_spaces_within_if_parentheses = false ij_java_spaces_within_method_call_parentheses = false @@ -250,9 +333,13 @@ 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_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = ij_java_subclass_name_suffix = Impl ij_java_ternary_operation_signs_on_next_line = false ij_java_ternary_operation_wrap = on_every_item +ij_java_test_name_prefix = ij_java_test_name_suffix = Test ij_java_throws_keyword_wrap = normal ij_java_throws_list_wrap = off @@ -268,6 +355,303 @@ ij_java_wrap_comments = false ij_java_wrap_first_method_in_call_chain = true ij_java_wrap_long_lines = false +[*.less] +indent_size = 2 +indent_style = space +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_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_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.proto] +indent_size = 2 +indent_style = space +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false + +[*.sass] +indent_size = 2 +indent_style = space +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_line_comment_add_space = false +ij_sass_line_comment_at_first_column = 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 + +[*.scala] +ij_continuation_indent_size = 4 +ij_smart_tabs = true +ij_scala_align_composite_pattern = true +ij_scala_align_extends_with = 0 +ij_scala_align_group_field_declarations = false +ij_scala_align_if_else = false +ij_scala_align_in_columns_case_branch = false +ij_scala_align_multiline_binary_operation = false +ij_scala_align_multiline_chained_methods = false +ij_scala_align_multiline_for = true +ij_scala_align_multiline_parameters = false +ij_scala_align_multiline_parameters_in_calls = false +ij_scala_align_multiline_parenthesized_expression = false +ij_scala_align_parameter_types_in_multiline_declarations = 0 +ij_scala_align_tuple_elements = false +ij_scala_alternate_continuation_indent_for_params = 4 +ij_scala_binary_operation_wrap = off +ij_scala_blank_lines_after_anonymous_class_header = 0 +ij_scala_blank_lines_after_class_header = 0 +ij_scala_blank_lines_after_imports = 1 +ij_scala_blank_lines_after_package = 1 +ij_scala_blank_lines_around_class = 1 +ij_scala_blank_lines_around_class_in_inner_scopes = 0 +ij_scala_blank_lines_around_field = 0 +ij_scala_blank_lines_around_field_in_inner_scopes = 0 +ij_scala_blank_lines_around_field_in_interface = 0 +ij_scala_blank_lines_around_method = 1 +ij_scala_blank_lines_around_method_in_inner_scopes = 1 +ij_scala_blank_lines_around_method_in_interface = 1 +ij_scala_blank_lines_before_class_end = 0 +ij_scala_blank_lines_before_imports = 1 +ij_scala_blank_lines_before_method_body = 0 +ij_scala_blank_lines_before_package = 0 +ij_scala_block_brace_style = end_of_line +ij_scala_block_comment_add_space = false +ij_scala_block_comment_at_first_column = true +ij_scala_call_parameters_new_line_after_lparen = 0 +ij_scala_call_parameters_right_paren_on_new_line = false +ij_scala_call_parameters_wrap = off +ij_scala_case_clause_brace_force = never +ij_scala_catch_on_new_line = false +ij_scala_class_annotation_wrap = split_into_lines +ij_scala_class_brace_style = end_of_line +ij_scala_closure_brace_force = never +ij_scala_do_not_align_block_expr_params = true +ij_scala_do_not_indent_case_clause_body = false +ij_scala_do_not_indent_tuples_close_brace = true +ij_scala_do_while_brace_force = never +ij_scala_else_on_new_line = false +ij_scala_enable_scaladoc_formatting = true +ij_scala_enforce_functional_syntax_for_unit = true +ij_scala_extends_keyword_wrap = off +ij_scala_extends_list_wrap = off +ij_scala_field_annotation_wrap = split_into_lines +ij_scala_finally_brace_force = never +ij_scala_finally_on_new_line = false +ij_scala_for_brace_force = never +ij_scala_for_statement_wrap = off +ij_scala_formatter = 0 +ij_scala_if_brace_force = never +ij_scala_implicit_value_class_prefix = +ij_scala_implicit_value_class_suffix = Ops +ij_scala_indent_braced_function_args = true +ij_scala_indent_case_from_switch = true +ij_scala_indent_fewer_braces_in_method_call_chains = false +ij_scala_indent_first_parameter = true +ij_scala_indent_first_parameter_clause = false +ij_scala_indent_type_arguments = true +ij_scala_indent_type_parameters = true +ij_scala_indent_yield_after_one_line_enumerators = true +ij_scala_keep_blank_lines_before_right_brace = 2 +ij_scala_keep_blank_lines_in_code = 2 +ij_scala_keep_blank_lines_in_declarations = 2 +ij_scala_keep_comments_on_same_line = true +ij_scala_keep_first_column_comment = false +ij_scala_keep_indents_on_empty_lines = true +ij_scala_keep_line_breaks = true +ij_scala_keep_one_line_lambdas_in_arg_list = false +ij_scala_keep_simple_blocks_in_one_line = false +ij_scala_keep_simple_methods_in_one_line = false +ij_scala_keep_xml_formatting = false +ij_scala_line_comment_add_space = false +ij_scala_line_comment_at_first_column = true +ij_scala_method_annotation_wrap = split_into_lines +ij_scala_method_brace_force = never +ij_scala_method_brace_style = end_of_line +ij_scala_method_call_chain_wrap = off +ij_scala_method_parameters_new_line_after_left_paren = true +ij_scala_method_parameters_right_paren_on_new_line = true +ij_scala_method_parameters_wrap = off +ij_scala_modifier_list_wrap = false +ij_scala_multiline_string_align_dangling_closing_quotes = false +ij_scala_multiline_string_closing_quotes_on_new_line = true +ij_scala_multiline_string_insert_margin_on_enter = true +ij_scala_multiline_string_margin_char = | +ij_scala_multiline_string_margin_indent = 2 +ij_scala_multiline_string_opening_quotes_on_new_line = true +ij_scala_multiline_string_process_margin_on_copy_paste = true +ij_scala_new_line_after_case_clause_arrow_when_multiline_body = false +ij_scala_newline_after_annotations = false +ij_scala_not_continuation_indent_for_params = false +ij_scala_parameter_annotation_wrap = off +ij_scala_parentheses_expression_new_line_after_left_paren = false +ij_scala_parentheses_expression_right_paren_on_new_line = false +ij_scala_place_closure_parameters_on_new_line = false +ij_scala_place_self_type_on_new_line = true +ij_scala_prefer_parameters_wrap = false +ij_scala_preserve_space_after_method_declaration_name = false +ij_scala_reformat_on_compile = false +ij_scala_replace_case_arrow_with_unicode_char = false +ij_scala_replace_for_generator_arrow_with_unicode_char = false +ij_scala_replace_lambda_with_greek_letter = false +ij_scala_replace_map_arrow_with_unicode_char = false +ij_scala_scalafmt_config_path = +ij_scala_scalafmt_fallback_to_default_settings = false +ij_scala_scalafmt_reformat_on_files_save = false +ij_scala_scalafmt_show_invalid_code_warnings = true +ij_scala_scalafmt_use_intellij_formatter_for_range_format = true +ij_scala_sd_align_exception_comments = true +ij_scala_sd_align_list_item_content = true +ij_scala_sd_align_other_tags_comments = true +ij_scala_sd_align_parameters_comments = true +ij_scala_sd_align_return_comments = true +ij_scala_sd_blank_line_after_parameters_comments = false +ij_scala_sd_blank_line_after_return_comments = false +ij_scala_sd_blank_line_before_parameters = false +ij_scala_sd_blank_line_before_tags = true +ij_scala_sd_blank_line_between_parameters = false +ij_scala_sd_keep_blank_lines_between_tags = true +ij_scala_sd_preserve_spaces_in_tags = false +ij_scala_space_after_comma = true +ij_scala_space_after_for_semicolon = true +ij_scala_space_after_modifiers_constructor = true +ij_scala_space_after_type_colon = true +ij_scala_space_before_brace_method_call = true +ij_scala_space_before_class_left_brace = true +ij_scala_space_before_for_parentheses = true +ij_scala_space_before_if_parentheses = true +ij_scala_space_before_infix_like_method_parentheses = false +ij_scala_space_before_infix_method_call_parentheses = false +ij_scala_space_before_infix_operator_like_method_call_parentheses = false +ij_scala_space_before_method_call_parentheses = false +ij_scala_space_before_method_left_brace = true +ij_scala_space_before_method_parentheses = true +ij_scala_space_before_type_colon = false +ij_scala_space_before_type_parameter_in_def_list = false +ij_scala_space_before_type_parameter_leading_context_bound_colon = false +ij_scala_space_before_type_parameter_leading_context_bound_colon_hk = true +ij_scala_space_before_type_parameter_list = false +ij_scala_space_before_type_parameter_rest_context_bound_colons = true +ij_scala_space_before_while_parentheses = true +ij_scala_space_inside_closure_braces = true +ij_scala_space_inside_self_type_braces = true +ij_scala_space_within_empty_method_call_parentheses = false +ij_scala_spaces_around_at_in_patterns = false +ij_scala_spaces_in_imports = false +ij_scala_spaces_in_one_line_blocks = false +ij_scala_spaces_within_brackets = false +ij_scala_spaces_within_for_parentheses = false +ij_scala_spaces_within_if_parentheses = false +ij_scala_spaces_within_method_call_parentheses = false +ij_scala_spaces_within_method_parentheses = false +ij_scala_spaces_within_parentheses = false +ij_scala_spaces_within_while_parentheses = false +ij_scala_special_else_if_treatment = true +ij_scala_trailing_comma_arg_list_enabled = true +ij_scala_trailing_comma_import_selector_enabled = false +ij_scala_trailing_comma_mode = trailing_comma_keep +ij_scala_trailing_comma_params_enabled = true +ij_scala_trailing_comma_pattern_arg_list_enabled = false +ij_scala_trailing_comma_tuple_enabled = false +ij_scala_trailing_comma_tuple_type_enabled = false +ij_scala_trailing_comma_type_params_enabled = false +ij_scala_try_brace_force = never +ij_scala_type_annotation_exclude_constant = true +ij_scala_type_annotation_exclude_in_dialect_sources = true +ij_scala_type_annotation_exclude_in_test_sources = false +ij_scala_type_annotation_exclude_member_of_anonymous_class = false +ij_scala_type_annotation_exclude_member_of_private_class = false +ij_scala_type_annotation_exclude_when_type_is_stable = true +ij_scala_type_annotation_function_parameter = false +ij_scala_type_annotation_implicit_modifier = true +ij_scala_type_annotation_local_definition = false +ij_scala_type_annotation_private_member = false +ij_scala_type_annotation_protected_member = true +ij_scala_type_annotation_public_member = true +ij_scala_type_annotation_structural_type = true +ij_scala_type_annotation_underscore_parameter = false +ij_scala_type_annotation_unit_type = true +ij_scala_use_alternate_continuation_indent_for_params = false +ij_scala_use_scala3_indentation_based_syntax = true +ij_scala_use_scaladoc2_formatting = true +ij_scala_variable_annotation_wrap = off +ij_scala_while_brace_force = never +ij_scala_while_on_new_line = false +ij_scala_wrap_before_with_keyword = false +ij_scala_wrap_first_method_in_call_chain = false +ij_scala_wrap_long_lines = false + +[*.scss] +indent_size = 2 +indent_style = space +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_block_comment_add_space = false +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_line_comment_add_space = false +ij_scss_line_comment_at_first_column = 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 + +[*.vue] +indent_size = 2 +indent_style = space +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + [.editorconfig] ij_editorconfig_align_group_field_declarations = false ij_editorconfig_space_after_colon = false @@ -276,7 +660,7 @@ ij_editorconfig_space_before_colon = false ij_editorconfig_space_before_comma = false ij_editorconfig_spaces_around_assignment_operators = true -[{*.ant,*.fxml,*.isc,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +[{*.ant,*.fxml,*.isc,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] ij_smart_tabs = true ij_xml_align_attributes = false ij_xml_align_text = false @@ -297,7 +681,367 @@ ij_xml_space_inside_empty_tag = true ij_xml_text_wrap = normal ij_xml_use_custom_settings = false -[{*.gant,*.groovy,*.gson,*.gy}] +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_smart_tabs = true +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 = false +ij_typescript_align_multiline_parameters = false +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_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = true +ij_typescript_call_parameters_right_paren_on_new_line = true +ij_typescript_call_parameters_wrap = normal +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_enum_constants_wrap = on_every_item +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 = true +ij_typescript_for_statement_right_paren_on_new_line = true +ij_typescript_for_statement_wrap = on_every_item +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 = false +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 = false +ij_typescript_keep_indents_on_empty_lines = true +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 = true +ij_typescript_method_parameters_right_paren_on_new_line = true +ij_typescript_method_parameters_wrap = on_every_item +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = true +ij_typescript_parentheses_expression_right_paren_on_new_line = true +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_property_prefix = +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 = true +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 = false +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 = auto +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 = true +ij_shell_use_unix_line_separator = true + +[{*.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 = true +ij_javascript_array_initializer_right_brace_on_new_line = true +ij_javascript_array_initializer_wrap = on_every_item +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_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = true +ij_javascript_call_parameters_right_paren_on_new_line = true +ij_javascript_call_parameters_wrap = on_every_item +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = false +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 = remove +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 = if_multiline +ij_javascript_for_statement_new_line_after_left_paren = true +ij_javascript_for_statement_right_paren_on_new_line = true +ij_javascript_for_statement_wrap = on_every_item +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 = false +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 = true +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 = true +ij_javascript_method_parameters_right_paren_on_new_line = true +ij_javascript_method_parameters_wrap = on_every_item +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_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_property_prefix = +ij_javascript_reformat_c_style_comments = true +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 = true +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 = true +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 = on_every_item +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = false +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = false +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.comp,*.frag,*.fsh,*.geom,*.glsl,*.tesc,*.tese,*.vert,*.vsh}] +ij_glsl_keep_indents_on_empty_lines = true + +[{*.ft,*.vm,*.vsl}] +ij_smart_tabs = true +ij_vtl_keep_indents_on_empty_lines = true + +[{*.gant,*.groovy,*.gy}] ij_smart_tabs = true ij_groovy_align_group_field_declarations = false ij_groovy_align_multiline_array_initializer_expression = false @@ -344,6 +1088,7 @@ 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 = true +ij_groovy_enable_groovydoc_formatting = true ij_groovy_enum_constants_wrap = on_every_item ij_groovy_extends_keyword_wrap = normal ij_groovy_extends_list_wrap = off @@ -353,6 +1098,12 @@ ij_groovy_for_brace_force = never ij_groovy_for_statement_new_line_after_left_paren = true ij_groovy_for_statement_right_paren_on_new_line = true ij_groovy_for_statement_wrap = on_every_item +ij_groovy_ginq_general_clause_wrap_policy = 2 +ij_groovy_ginq_having_wrap_policy = 1 +ij_groovy_ginq_indent_having_clause = true +ij_groovy_ginq_indent_on_clause = true +ij_groovy_ginq_on_wrap_policy = 1 +ij_groovy_ginq_space_after_keyword = true ij_groovy_if_brace_force = never ij_groovy_import_annotation_wrap = 2 ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* @@ -376,6 +1127,7 @@ 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_add_space_on_reformat = 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 @@ -481,6 +1233,109 @@ ij_groovy_while_on_new_line = false ij_groovy_wrap_chain_calls_after_dot = false ij_groovy_wrap_long_lines = false +[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.kts}] +ij_smart_tabs = true +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 = 1 +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_add_space = false +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 = on_every_item +ij_kotlin_extends_list_wrap = on_every_item +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 = false +ij_kotlin_keep_indents_on_empty_lines = true +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = on_every_item +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.*,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,.conf,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config,mcmod.info,meatball_from_mutton.json,pack.mcmeta}] +ij_smart_tabs = true +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = true +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +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,*.ng,*.sht,*.shtm,*.shtml}] ij_smart_tabs = true ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 @@ -491,7 +1346,7 @@ ij_html_block_comment_add_space = false 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 = none +ij_html_do_not_indent_children_of_tags = 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 = 0 @@ -510,19 +1365,56 @@ ij_html_space_around_equality_in_attribute = false ij_html_space_inside_empty_tag = true ij_html_text_wrap = off +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] +ij_smart_tabs = true +ij_jsp_jsp_prefer_comma_separated_import_list = false +ij_jsp_keep_indents_on_empty_lines = true + +[{*.jspx,*.tagx}] +ij_smart_tabs = true +ij_jspx_keep_indents_on_empty_lines = true + [{*.markdown,*.md}] ij_smart_tabs = true 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_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true ij_markdown_keep_indents_on_empty_lines = true +ij_markdown_keep_line_breaks_inside_text_blocks = true 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 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.pb,*.textproto}] +indent_size = 2 +indent_style = space +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false [{*.properties,spring.handlers,spring.schemas}] ij_properties_align_group_field_declarations = false @@ -530,3 +1422,23 @@ ij_properties_keep_blank_lines = true ij_properties_key_value_delimiter = equals ij_properties_spaces_around_key_value_delimiter = true +[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] +indent_style = space +ij_qute_keep_indents_on_empty_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +indent_style = space +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = true +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.gitignore b/.gitignore index 881a611..f02e4e8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,15 @@ .vscode/ .gradle/ .settings/ -/src/test/java/test/* -/src/test/resources/test/* #build /build/ /bin/ +/out/ +.metals/ +.bloop/ .project +lcoal.properties # debug dir /run/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index aa5e2ed..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "_book"] - path = _book - url = https://storage.sukazyo.cc/Eyre_S/morny-book.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..055ebfc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM eclipse-temurin:20-jdk as build +LABEL authors="A.C.Sukazyo Eyre" + +COPY . /app/source/ +WORKDIR /app + +RUN cd ./source \ +&& ./gradlew shadowJar -PdockerBuild \ +&& cd .. \ +&& cp ./source/build/libs/morny-coeur-docker-build.jar ./morny-coeur.jar +#&& rm -r ./source \ +#&& rm -r /root/.gradle \ + + +FROM eclipse-temurin:20-jre + +COPY --from=build /app/morny-coeur.jar /app/morny-coeur.jar +WORKDIR /app + +ENTRYPOINT ["java", "-jar", "morny-coeur.jar"] +CMD ["-q", "-v"] diff --git a/README.md b/README.md index 99bf217..22e0fb8 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,22 @@ [todo]: https://github.com/users/Eyre-S/projects/1 [artifact]: https://mvn.sukazyo.cc/#/releases/cc/sukazyo/morny-coeur -[tg4j]: https://github.com/pengrad/java-telegram-bot-api +[scala]: https://www.scala-lang.org/ [spotbugs]: https://spotbugs.github.io/ -[junit5]: https://junit.org/junit5/ +[tg4j]: https://github.com/pengrad/java-telegram-bot-api +[okhttp]: https://square.github.io/okhttp/ +[gson]: https://github.com/google/gson +[scalatest]: https://scalatest.org/
# ~~给所有喜欢morny的大家的~~ Morny Coeur 源代码 -~~"你们又有意见又不发issue这样子我很为难的啊"~~ +~~"and nobody cares."~~ ![social preview card](morny-github-social-preview-card@0.75x.png) -一个 telegram 上的服侍 A.C.Sukazyo Eyre 和它的花宫成员的 bot 的内核源 +一个 telegram 上的服侍 A.C.Sukazyo Eyre 和它的花宫成员的 bot 内核 [Task Listing][todo] | [~~BBS~~][issues] | [Published][artifact] @@ -32,6 +35,9 @@ [Java Telegram Bot API][tg4j] -[SpotBugs Annotations][spotbugs] | [JUnit 5][junit5] + +[okhttp] | [Gson][gson] + +[Scala][scala] | [SpotBugs Annotations][spotbugs] | [ScalaTest][scalatest]
diff --git a/_book b/_book deleted file mode 160000 index 3072bce..0000000 --- a/_book +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3072bcee8e498e87ecdd36958185ad423e80bcf3 diff --git a/build.gradle b/build.gradle index 0e2a6aa..e22d4d9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,72 @@ plugins { - id 'java' + id 'scala' id 'java-library' - id 'maven-publish' id 'application' - id 'com.github.johnrengelman.shadow' version '7.1.0' + id 'maven-publish' + id "io.github.ysohda.scalatest" version "0.32.1" + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'com.github.gmazzo.buildconfig' version '4.1.2' + id 'org.ajoberstar.grgit' version '5.2.0' } -group 'cc.sukazyo' -version VERSION -project.ext.archiveBaseName = 'Coeur_Morny_Cono' -project.ext.artifactId = 'morny-coeur' -mainClassName = 'cc.sukazyo.cono.morny.ServerMain' +import org.ajoberstar.grgit.Status + +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +final boolean proj_git = grgit!=null +final String proj_store = MORNY_CODE_STORE +final String proj_commit = proj_git ? grgit.head().id : null +final String proj_commit_path = MORNY_COMMIT_PATH +final boolean proj_clean = isCleanBuild() +if (!proj_git) + println "[MornyBuild] git repository not available for current working space! git version tag will be disabled." +else if (isCleanBuild()) { + println "git: clean build at ${grgit.head().id}" +} else { + final Status status = grgit.status() + println "git: non-clean-build" + if (!status.unstaged.allChanges.empty) { + println "git: unstaged changes" + listChanges(status.unstaged) + } + if (!status.staged.allChanges.empty) { + println "git: staged changes" + listChanges(status.staged) + } +} + +final String proj_group = 'cc.sukazyo' +final String proj_package = "${proj_group}.cono.morny" +final String proj_archive_name = MORNY_ARCHIVE_NAME +final String proj_application_main = "${proj_package}.ServerMain" + +final String proj_version_base = VERSION +final String proj_version_delta = VERSION_DELTA +final boolean proj_version_use_delta = Boolean.parseBoolean(USE_DELTA) +final String proj_version = proj_version_base + (proj_version_use_delta ? "-δ${proj_version_delta}" : "") +final String proj_version_full = proj_version + (proj_git ? "+git.${proj_commit.substring(0, 8)}" + (proj_clean?"":".δ") : "") +final String proj_version_codename = CODENAME +final long proj_code_time = proj_clean ? grgit.head().dateTime.toInstant().toEpochMilli() : System.currentTimeMillis() + +final JavaVersion proj_java = JavaVersion.VERSION_17 +final Charset proj_file_encoding = StandardCharsets.UTF_8 +final proj_scala_api = 3 +//final proj_scala_lib = proj_scala_api+'.4.0-RC1-bin-20230901-89e8dba-NIGHTLY' +final proj_scala_lib = proj_scala_api+'.3.1' +String publish_local_url = null +String publish_remote_url = null +String publish_remote_username = null +String publish_remote_password = null +if (project.hasProperty("publishLocalArchiveRepoUrl")) publish_local_url = publishLocalArchiveRepoUrl +if (project.hasProperty("publishMvnRepoUrl")) { + publish_remote_url = publishMvnRepoUrl + publish_remote_username = publishMvnRepoUsername + publish_remote_password = publishMvnRepoPassword +} + +group proj_group +version proj_version_full repositories { mavenCentral() @@ -19,86 +75,142 @@ repositories { dependencies { - compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${libSpotbugsVersion}" + api "org.scala-lang:scala3-library_3:${proj_scala_lib}" + compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${lib_spotbugs_v}" - api "cc.sukazyo:messiva:${libMessivaVersion}" + implementation "cc.sukazyo:messiva:${lib_messiva_v}" + implementation "cc.sukazyo:resource-tools:${lib_resourcetools_v}" - implementation "com.github.pengrad:java-telegram-bot-api:${libJavaTelegramBotApiVersion}" + implementation "com.github.pengrad:java-telegram-bot-api:${lib_javatelegramapi_v}" + implementation "com.squareup.okhttp3:okhttp:${lib_okhttp_v}" + implementation "com.google.code.gson:gson:${lib_gson_v}" - testImplementation "org.junit.jupiter:junit-jupiter-api:${libJunitVersion}" - testImplementation "org.junit.jupiter:junit-jupiter-params:${libJunitVersion}" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${libJunitVersion}" + testImplementation "org.scalatest:scalatest_$proj_scala_api:${lib_scalatest_v}" + testImplementation "org.scalatest:scalatest-freespec_$proj_scala_api:${lib_scalatest_v}" + testRuntimeOnly "org.scala-lang.modules:scala-xml_$proj_scala_api:${lib_scalamodule_xml_v}" + testRuntimeOnly 'com.vladsch.flexmark:flexmark-all:0.64.6' // for generating HTML report // required by gradle-scalatest plugin } -task updateVersionCode { - ant.replaceregexp(match:'VERSION = ["a-zA-Z0-9.\\-_+@]+;', replace:"VERSION = \"$project.version\";", flags:'g', byline:true) { - fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java') - } - ant.replaceregexp(match:'CODENAME = ["a-zA-Z0-9]+;', replace:"CODENAME = \"${CODENAME}\";", flags:'g', byline:true) { - fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java') - } - ant.replaceregexp(match:'COMPILE_TIMESTAMP = [0-9]+L;', replace:"COMPILE_TIMESTAMP = ${System.currentTimeMillis()}L;", flags:'g', byline:true) { - fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java') - } -} - -compileJava.dependsOn updateVersionCode - -test { - useJUnitPlatform() -} - java { - - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - withSourcesJar() +} + +tasks.withType(JavaCompile).configureEach { + + sourceCompatibility proj_java.getMajorVersion() + targetCompatibility proj_java.getMajorVersion() + + options.encoding = proj_file_encoding.name() } -tasks.withType(JavaCompile) { - options.encoding = "UTF-8" +tasks.withType(ScalaCompile).configureEach { + + sourceCompatibility proj_java.getMajorVersion() + targetCompatibility proj_java.getMajorVersion() + + options.encoding = proj_file_encoding.name() + scalaCompileOptions.encoding = proj_file_encoding.name() + + scalaCompileOptions.additionalParameters.add "-language:postfixOps" + } -tasks.withType(Javadoc) { - options.encoding = 'UTF-8' - options.docEncoding = 'UTF-8' - options.charSet = 'UTF-8' +tasks.withType(Javadoc).configureEach { + options.encoding = proj_file_encoding.name() } -tasks.test { - useJUnitPlatform() +//tasks.withType(ScalaDoc).configureEach { +//} + +test { +} + +application { + mainClass = proj_application_main +} + +buildConfig { + + packageName(proj_package) + + buildConfigField('String', 'VERSION', "\"${proj_version}\"") + buildConfigField('String', 'VERSION_FULL', "\"${proj_version_full}\"") + buildConfigField('String', 'VERSION_BASE', "\"${proj_version_base}\"") + buildConfigField('String', 'VERSION_DELTA', proj_version_use_delta ? "\"${proj_version_delta}\"" : "null") + buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"") + buildConfigField('long', 'CODE_TIMESTAMP', "${proj_code_time}L") + buildConfigField('String', 'COMMIT', proj_git ? "\"${proj_commit}\"" : "null") + buildConfigField('boolean', 'CLEAN_BUILD', "${proj_clean}") + buildConfigField('String', 'CODE_STORE', proj_store==""?"null":"\"${proj_store}\"") + buildConfigField('String', 'COMMIT_PATH', proj_commit_path==""?"null":"\"${proj_commit_path}\"") + +} + +tasks.withType(Jar).configureEach { + archiveBaseName.set proj_archive_name } shadowJar { - archiveBaseName.set("${project.ext.archiveBaseName}") - archiveVersion.set("${project.version}") - archiveClassifier.set("fat") + + archiveClassifier.set "fat" + + if (project.hasProperty("dockerBuild")) { + println "shadow-jar: using docker build name" + archiveVersion.set "" + archiveClassifier.set "docker-build" + } + +} + +@SuppressWarnings('GrMethodMayBeStatic') +boolean isCleanBuild () { + if (grgit == null) return false + Set changes = grgit.status().unstaged.allChanges + grgit.status().staged.allChanges + for (String file in changes) { + if (file.startsWith("src/")) return false + if (file == "build.gradle") return false + if (file == "gradle.properties") return false + } + return true +} + +void listChanges (Status.Changes listing) { + for (String file in listing.added) + println " add: ${file}" + for (String file in listing.modified) + println " mod: ${file}" + for (String file in listing.removed) + println " del: ${file}" } publishing { repositories{ - maven { - name 'builds' - url publishLocalArchiveRepoUrl + if (publish_local_url != null) maven { + name 'archives' + url publish_local_url } - maven { + if (publish_remote_url != null) maven { name '-ws-' - url publishMvnRepoUrl + url publish_remote_url credentials { - username publishMvnRepoUsername - password publishMvnRepoPassword + username publish_remote_username + password publish_remote_password } } } publications { + //noinspection GroovyAssignabilityCheck main (MavenPublication) { + //noinspection GroovyAssignabilityCheck from components.java - groupId = project.group - artifactId = project.ext.artifactId - version = project.version + //noinspection GroovyAssignabilityCheck + groupId = proj_group + //noinspection GroovyAssignabilityCheck + artifactId = proj_archive_name + //noinspection GroovyAssignabilityCheck + version = proj_version } } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4d8cce6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,4 @@ +services: + coeur-app: + build: . + command: -v diff --git a/gradle.properties b/gradle.properties index 338d93f..0620ae1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,28 @@ ## Core -VERSION = 0.8.0.11 +MORNY_ARCHIVE_NAME = morny-coeur -CODENAME = putian +MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono +MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s + +VERSION = 1.0.0 + +USE_DELTA = false +VERSION_DELTA = + +CODENAME = beiping # dependencies -libSpotbugsVersion = 4.7.2 +lib_spotbugs_v = 4.7.3 +lib_scalamodule_xml_v = 2.2.0 -libMessivaVersion = 0.1.0.1 +lib_messiva_v = 0.1.1 +lib_resourcetools_v = 0.2.2 -libJavaTelegramBotApiVersion = 5.6.0 +lib_javatelegramapi_v = 6.2.0 -libJunitVersion = 5.9.0 +lib_okhttp_v = 4.11.0 +lib_gson_v = 2.10.1 + +lib_scalatest_v = 3.2.17 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102..db9a6b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java b/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java deleted file mode 100644 index 8735b7d..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java +++ /dev/null @@ -1,10 +0,0 @@ -package cc.sukazyo.cono.morny; - -/** - * the final field that will be updated by gradle automatically. - */ -public class GradleProjectConfigures { - public static final String VERSION = "0.8.0.11"; - public static final String CODENAME = "putian"; - public static final long COMPILE_TIMESTAMP = 1667376095614L; -} diff --git a/src/main/java/cc/sukazyo/cono/morny/Log.java b/src/main/java/cc/sukazyo/cono/morny/Log.java deleted file mode 100644 index b58c825..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/Log.java +++ /dev/null @@ -1,18 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.messiva.Logger; -import cc.sukazyo.messiva.appender.ConsoleAppender; - -/** - * Morny 的 log 管理器 - */ -public class Log { - - /** - * Morny 的 Logger 实例, - * messiva 更新 - * @since 0.4.1.1 - */ - public static final Logger logger = new Logger(new ConsoleAppender()); - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java deleted file mode 100644 index 9c0cee9..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java +++ /dev/null @@ -1,320 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.cono.morny.bot.api.OnUpdate; -import cc.sukazyo.cono.morny.bot.command.MornyCommands; -import cc.sukazyo.cono.morny.bot.event.EventListeners; -import cc.sukazyo.cono.morny.bot.query.MornyQueries; -import cc.sukazyo.cono.morny.daemon.MornyDaemons; -import cc.sukazyo.cono.morny.daemon.TrackerDataManager; -import cc.sukazyo.cono.morny.util.tgapi.ExtraAction; -import com.pengrad.telegrambot.TelegramBot; -import com.pengrad.telegrambot.impl.FileApi; -import com.pengrad.telegrambot.model.User; -import com.pengrad.telegrambot.request.GetMe; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Set; - -import static cc.sukazyo.cono.morny.Log.logger; - -/** - * Morny Cono 核心
- * - 的程序化入口类,保管着 morny 的核心属性
- */ -public class MornyCoeur { - - /** 当前程序的 Morny Coeur 实例 */ - private static MornyCoeur INSTANCE; - - /** 当前 Morny 的{@link MornyTrusted 信任验证机}实例 */ - private final MornyTrusted trusted; - /** 当前 Morny 的 telegram 命令管理器 */ - private final MornyCommands commandManager = new MornyCommands(); - private final MornyQueries queryManager = new MornyQueries(); - - /** morny 的 bot 账户 */ - private final TelegramBot account; - private final ExtraAction extraActionInstance; - private final boolean isRemoveCommandListWhenExit; - /** - * morny 的 bot 账户的用户名
- *
- * 这个字段将会在登陆成功后赋值为登录到的 bot 的 username。 - * 它应该是和 {@link #account} 的 username 同步的
- *
- * 如果在登陆之前就定义了此字段,则登陆代码会验证登陆的 bot 的 username - * 是否与定义的 username 符合。如果不符合则会报错。 - */ - public final String username; - /** - * morny 的 bot 账户的 telegram id
- *
- * 这个字段将会在登陆成功后赋值为登录到的 bot 的 id。 - */ - public final long userid; - /** - * morny 的事件忽略前缀时间
- *
- * {@link cc.sukazyo.cono.morny.bot.event.OnUpdateTimestampOffsetLock} - * 会根据这里定义的时间戳取消掉比此时间更早的事件链 - */ - public final long latestEventTimestamp; - /** - * morny 主程序启动时间
- * 用于统计数据 - */ - public static final long coeurStartTimestamp = System.currentTimeMillis(); - - public static final long DINNER_CHAT_ID = -1001707106392L; - - private record LogInResult(TelegramBot account, String username, long userid) { } - - /** - * 执行 bot 初始化 - * - * @param botKey bot 的 telegram bot api token - * @param botUsername bot 的 username 限定。如果为 null 则表示不限定, - * 如果指定,则登录时会检查所登陆的 bot 的用户名是否与此相等 - * @param master morny 实例所信任的主人的 id。用于初始化 {@link #trusted} - * @param trustedChat morny 实例所信任的群组的 id。用于初始化 {@link #trusted} - * @param latestEventTimestamp 事件处理器会处理事件的最早时间戳 —— - * 只有限定的 message 事件会受此影响。 - * 单位为毫秒 - */ - private MornyCoeur ( - @Nullable String botApi, @Nullable String botApi4File, - @Nonnull String botKey, @Nullable String botUsername, - long master, long trustedChat, Set trustedRDinner, - long latestEventTimestamp, - boolean isRemoveCommandListWhenExit - ) { - - this.latestEventTimestamp = latestEventTimestamp; - this.isRemoveCommandListWhenExit = isRemoveCommandListWhenExit; - configureSafeExit(); - - logger.info("args key:\n " + botKey); - if (botUsername != null) { - logger.info("login as:\n " + botUsername); - } - - try { - final LogInResult loginResult = login(botApi, botApi4File, botKey, botUsername); - this.account = loginResult.account; - this.username = loginResult.username; - this.userid = loginResult.userid; - this.trusted = new MornyTrusted(master, trustedChat, trustedRDinner); - StringBuilder trustedReadersDinnerIds = new StringBuilder(); - trusted.getTrustedReadersOfDinnerSet().forEach(id -> trustedReadersDinnerIds.append("\n ").append(id)); - logger.info(String.format(""" - trusted param set: - - master (id) - %d - - trusted chat (id) - %d - - trusted reader-of-dinner (id)%s""", - master, trustedChat, trustedReadersDinnerIds - )); - } - catch (Exception e) { - RuntimeException ex = new RuntimeException("Cannot login to bot/api. :\n " + e.getMessage()); - logger.error(ex.getMessage()); - throw ex; - } - - this.extraActionInstance = ExtraAction.as(account); - - logger.info("Bot login succeed."); - - } - - /** - * 向外界暴露的 morny 初始化入口. - *

- * 如果 morny 已经初始化,则不会进行初始化,抛出错误消息并直接退出方法。 - * - * @see #MornyCoeur 程序初始化方法 - */ - public static void main ( - @Nullable String botApi, @Nullable String botApi4File, - @Nonnull String botKey, @Nullable String botUsername, - long master, long trustedChat, Set trustedRDinner, long latestEventTimestamp, - boolean isAutomaticResetCommandList, boolean isRemoveCommandListWhenExit - ) { - if (INSTANCE == null) { - logger.info("Coeur Starting"); - INSTANCE = new MornyCoeur( - botApi, botApi4File, - botKey, botUsername, - master, trustedChat, trustedRDinner, - latestEventTimestamp, - isRemoveCommandListWhenExit - ); - MornyDaemons.start(); - logger.info("start telegram events listening"); - EventListeners.registerAllListeners(); - INSTANCE.account.setUpdatesListener(OnUpdate::onNormalUpdate); - if (isAutomaticResetCommandList) { - logger.info("resetting telegram command list"); - commandManager().automaticUpdateList(); - } - logger.info("Coeur start complete"); - return; - } - logger.error("Coeur already started!!!"); - } - - /** - * 向所有的数据管理器发起保存数据的指令 - * @since 0.4.3.0 - */ - public void saveDataAll () { - TrackerDataManager.save(); - } - - /** - * 用于退出时进行缓存的任务处理等进行安全退出 - */ - private void exitCleanup () { - logger.info("clean:save tracker data."); - MornyDaemons.stop(); - if (isRemoveCommandListWhenExit) { - commandManager.automaticRemoveList(); - } - } - - /** - * 为程序在虚拟机上添加退出钩子 - */ - private void configureSafeExit () { - Runtime.getRuntime().addShutdownHook(new Thread(this::exitCleanup, "exit-cleaning")); - } - - /** - * 登录 bot
- *
- * 会反复尝试三次进行登录。如果登录失败,则会直接抛出 RuntimeException 结束处理。 - * 会通过 GetMe 动作验证是否连接上了 telegram api 服务器, - * 同时也要求登录获得的 username 和 {@link #username} 声明值相等 - * - * @param api bot client 将会连接到的 telegram bot api 位置 - * @param api4File bot client 将会连接到的 telegram file api 位置,如果不指定则会跟随 {@code api} 选项的设定 - * @param key bot 的 api-token - * @param requireName 要求登录到的需要的 username,如果登陆后的 username 与此不同则会报错退出 - * @return 成功登录后的 {@link TelegramBot} 对象 - */ - @Nonnull - private static LogInResult login ( - @Nullable String api, @Nullable String api4File, - @Nonnull String key, @Nullable String requireName - ) { - final TelegramBot.Builder accountConfig = new TelegramBot.Builder(key); - boolean isCustomApi = false; - String apiUrlSet = "https://api.telegram.org/bot"; - String api4FileUrlSet = FileApi.FILE_API; - if (api != null) { - api = api.endsWith("/") ? api.substring(0, api.length() - 1) : api; - accountConfig.apiUrl(apiUrlSet = api.endsWith("/bot")? api : api + "/bot"); - isCustomApi = true; - } - if (api4File != null) { - api4File = api4File.endsWith("/") ? api4File : api4File + "/"; - accountConfig.fileApiUrl(api4FileUrlSet = api4File.endsWith("/file/bot")? api4File : api4File + "/file/bot"); - isCustomApi = true; - } else if (api != null && !api.endsWith("/bot")) { - accountConfig.fileApiUrl(api4FileUrlSet = api + "/file/bot"); - } - if (isCustomApi) { - logger.info(String.format(""" - Telegram Bot API set to : - - %s - - %s""", - apiUrlSet, api4FileUrlSet - )); - } - final TelegramBot account = accountConfig.build(); - logger.info("Trying to login..."); - for (int i = 1; i < 4; i++) { - if (i != 1) logger.info("retrying..."); - try { - final User remote = account.execute(new GetMe()).user(); - if (requireName != null && !requireName.equals(remote.username())) - throw new RuntimeException("Required the bot @" + requireName + " but @" + remote.username() + " logged in!"); - logger.info("Succeed login to @" + remote.username()); - return new LogInResult(account, remote.username(), remote.id()); - } catch (Exception e) { - e.printStackTrace(System.out); - logger.error("login failed."); - } - } - throw new RuntimeException("Login failed.."); - } - - /** - * @see #saveDataAll() - * @since 0.4.3.0 - */ - public static void callSaveData () { - INSTANCE.saveDataAll(); - logger.info("done all save action."); - } - - /** - * 获取登录成功后的 telegram bot 对象 - * - * @return {@link #account MornyCoeur.account} - */ - @Nonnull - public static TelegramBot getAccount () { - return INSTANCE.account; - } - - /** - * 获取登录 bot 的 username - * - * @return {@link #username MornyCoeur.username} - */ - @Nonnull - public static String getUsername () { - return INSTANCE.username; - } - - /** - * - * 获取忽略时间点 - * - * @return {@link #latestEventTimestamp MornyCoeur.latestEventTimestamp} - */ - public static long getLatestEventTimestamp () { - return INSTANCE.latestEventTimestamp; - } - - /** - * 获取 Morny 的{@link MornyTrusted 信任验证机} - * - * @return {@link #trusted MornyCoeur.trusted} - */ - @Nonnull - public static MornyTrusted trustedInstance () { - return INSTANCE.trusted; - } - - @Nonnull - public static MornyCommands commandManager () { - return INSTANCE.commandManager; - } - - @Nonnull - public static MornyQueries queryManager () { - return INSTANCE.queryManager; - } - - @Nonnull - public static ExtraAction extra () { - return INSTANCE.extraActionInstance; - } - - public static long getUserid () { return INSTANCE.userid; } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyHello.java b/src/main/java/cc/sukazyo/cono/morny/MornyHello.java deleted file mode 100644 index 8a3725b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/MornyHello.java +++ /dev/null @@ -1,70 +0,0 @@ -package cc.sukazyo.cono.morny; - -/** - * {@link #MORNY_PREVIEW_IMAGE_ASCII} 静态数据存放类 - */ -@SuppressWarnings("all") -public class MornyHello { - - /** - * 系统的开屏欢迎语 ASCII 字符画字段 - */ - public static final String MORNY_PREVIEW_IMAGE_ASCII = """ - ttt///t/////fucj(\\tvnxtf{< .' .. .:i` . . ^!`l|-^i+,!_[:1/|{i?//\\//jf\\\\\\///\\\\\\\\//\\\\\\//////\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\/\\\\\\\\/\\\\//\\\\\\///\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\fnncvvU0O00QCx!!". .. ` \s - tt//////////\\jzjrucnjt/?{j,,"' . .' .. .":. .;{: ' "`.,1(<."i?)\\(-}\\\\\\(((\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\///\\//////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|\\\\\\\\\\\\///\\\\///\\\\\\\\\\\\\\\\|\\\\\\\\\\\\|\\\\\\\\\\\\\\\\tvXvuXcxn/[Il)({_:.. ."` .,\s - //////////////////\\////|)/([}-_<+[]>.^^""[<'`^` .''""`'.`'`"i! ^!>l:' :<" !!.IiI`+l^^`i>_<`??)1;^{\\\\\\\\\\{|({({|/\\]I)\\\\()\\(]}|\\\\||\\|||\\/\\\\\\\\\\\\|||\\\\\\\\\\\\\\\\//\\\\\\\\/\\\\\\\\||\\\\\\\\\\\\\\\\//\\\\\\\\\\\\\\\\\\\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\\\\\\\\\\\\\////\\\\\\\\\\\\\\\\\\//|{{?{|)[[-; - ttt/tt//////////////////{)(\\t(/tt/1~I}{-1\\_^])1_+[{|(?"<1~>>+!+[}11)}[(1}]};^1\\|~_1}{I:-1(I+)(|))|\\\\/////////\\\\////\\\\\\/////\\\\\\\\\\\\\\\\\\\\\\\\\\\\/\\//\\\\///\\//||\\////|)(//\\\\///){\\/\\(11|///({)//({[1\\\\\\\\\\\\\\\\\\\\|\\/\\\\\\/\\//////////\\\\\\\\\\\\\\\\\\//\\\\\\\\\\///////\\|\\\\\\\\//////\\\\///\\ - tttt/////////////\\///////\\||///////t//|(|)|}|\\/(\\\\(//(l_{{. ... ">+<^'I!: ^<(\\\\1}1//\\\\\\//////////\\\\///\\/\\///\\\\\\\\\\\\//\\\\//\\\\\\\\\\\\\\\\\\\\\\\\\\\\///\\(/\\{ - t////////////////////////////////////////////////////////\\/\\\\///////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\/\\\\\\\\\\\\||\\|\\\\\\\\\\|\\\\\\\\/\\\\|\\\\\\\\\\\\////((|///}!:,":,^`. .;' ' '^..':. ^!;. .^^ '^^`. '' ...I[{!>:^;_i:'~\\ttt/////tt//\\\\////////////\\\\\\\\/\\\\\\\\\\\\/\\\\\\\\/\\\\\\\\\\)}-+[+I??i - ttt////////////////////////////////////////////////////////\\\\//\\//\\\\/\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\||\\\\\\/\\//\\\\\\\\\\\\\\\\\\/\\|\\\\\\////\\1;``^;<>+!">__+I `' .. "'. .;" ;;. .:^ ``,,;'` .;]I ,-_-|\\////t////t///////\\/\\\\\\\\//\\\\\\\\\\\\//\\\\\\///////-II1ttt///tttt/////////\\/\\/////\\\\\\\\////t|+<}?!-]l<{[[1-+] - t//////////////////////////////////////\\/////////\\////////////////\\//////\\//tttttttttt//////////////////////////)_)t)|}1f/{<.^,^:~: . .. '''^:-|/> '-/}-_?\\/)-{?(//\\(\\tt////\\///\\\\\\\\\\\\\\///t1.;); .l~` '" - ///////////////////////////////////////////tt/t(|tt//]+{t\\{][|////\\//////////ttttt///t//t/////////////\\//////|//{[|f}!l>~++~<<\\//]l~?])tt//\\\\\\\\\\/\\\\///\\\\|?<_}["^!;I^;]:. . - ////////////////////////////////tttt/|{[1)]~!!+>!<_(/|[-<"i!l,]tt//ttt/t////ttt//t///ttttttt////////t//t//ttt){+. :?^ '. l_-!+l;;;|!!>~~il!lllllllllll!!lI:`'. .' :I;]_}>,?tf:.+fft)l+1//\\~`'I-(//\\/t/|/(-1[)/?>>II:' '.`';-'` \s - /////////////////////////t//()\\1_<>il^'''' ,!>;.,.'{tti `~tf(`'-(|fffftttttttttt/tttttttttttt///tttft//(t|]?-+!^ ."`. `. ;!I,. .?{il-\\_!~<>>!lII;IllIIIIIllllllllllI;;:,:,' '"^`(f{+{>' .<{t(I!}/||t> ^(//}>;:1\\]: "[:"` ^<: . II.'.. \s - ///////////////ttt//tt((-!+}"'^. I, ,?<:' ,:;!>~',!_~{}-1]`^!}_+\\ttttttt/tttttttfff/tt\\(||]-?+;,:"l" '..'.. ?]l:" -(lI;,~?~!IIIlllI:IIlI;IIIIlllllllIIIIIllII!; . . '^^;~), "~!}\\/t//\\\\/_. '". '_i !i''' \s - tt//t///ttt///(]<>l>][l"'.`,. ^.^. ii ;; ~>>>. .i~I'^^<}), .;|tfftttttttttttf\\]}t-!,,I` .^ '. !: . .",I;. ^,I<)/-l:;llllllI;lIll;;IIIIlllIllIlIIlIllI;><. ' .;}". '.:+](ft\\}(t/t{;<\\{l^>}!^l\\/{>1/t(lI:I!+<<". ':" \s - t//tttt|?+!I!:' '` .`. ...... `^ "<^.;`^"'`,!".,^^^.,?)!. [f/+>(/tttft\\tff|+^,!' '^: >[,++:`' .I^ . _?!:^. ;~{/?//)! __::. ':. '. \s - t/\\}[{]",il'`!-<-]:`'^` .. '' .^+:'. .^'"i:`^. ';`:<_|>'.?/t/!"<)ffftf)]]!'II.,l ^' ''. '";" .' `Il, ;]>]j_;lI;ll;!!llIII>~IIIIII;: ;,~.',.<:`, 'I_|\\; .i|/]^ ?(}\\/////\\i' '' ....'^ \s - tf1<}i `^. `I` .I?"'. . . ^' .^' .'` .". >}_.I|t{_(tf({~,~(); ')t};.><,. .. . .. . . .]}^{j1IlllIlI!1IlllII?{IIIIIlI;[1!I;IIllIIlIIllI<]_;+/t\\(|\\/1,' "` ... \s - )+::((:^' ll .,` . . ..'. ' :+'`{tj{,l: ^;"..;!"^.I?' '~; .` .'. . `1+ [x?-:lIlll!]r-IllI~~{~I>lllll[i\\--+;;I~IIIII!l;x] "I"-<<_> >i.' l{}:itf/}[/\\)(\\}))|(:^^..'. `". \s - >.._f|i.:l,;^^''__. .^' `' "+,`]1i`!1_. ^l: .". .` '1I +JIt!IIlll;]\\) >1}c(_i(!IllI_l;(f. ,_";~~+^ .. .;-i '+([i+: !//1](||/\\(?:^..^^ \s - i'"}_,.` ''^... '. `. ^<`_> .. +x??_~]:[|!,.ll` . {+ ;Y[^|,>~IlIIf\\ {/;I!\\ [[-'<+l-{ _??]f\\n]lllI[!;1v` `+"]-}]~" ..'`l, ''i-` l+?\\\\\\/t{!)t[:' .^^ \s - ;,:: :,^..;:. . i+..;^ `_ ]]<-?l``-]' I>]?+. ^-|\\\\_I?]{t/?` .... \s - ^(\\]I^~?;."!" . .. . ^<^ :( >t) _[il>|+:(U<1nYQ0Xx\\> . .~xcXXYzx(n?IllI}">xCI .:1]_-" . .^. `}>!}((1-^,+?" .. \s - 1+,~I.(l' . ... ~|r:;`.+I?\\};+t) "".-?;lI>(;]xn. '_>]!+. '^'`l:11l[|((+?: . \s - [-`.':;..""' lv|. .:_(;I!u> ^,",^. .;?I]?IlI}n[^ ')(I' `tlII>x1" <}1{)l "~+ |[II;\\[:~zl .i;. .... `"i\\\\}]..!' ^.... \s - -" ` ' . ":` .|+<<;!\\U)>^ '^`' ^"I?)c-;j/ <1I~;` .!}\\(: .;"`' \s - ' 'l, .' ~[><+;!f()nn|]!:' ..^:!+1fcjx}}v!_)})>|n` ~^ ^;"'` .<+I<)/||\\i'"<"'^ `. \s - II ^}_'+_!fI?_/-jJjUr\\\\ucJJz\\|J>}?-j{]^ni" .;](),.;-<`' .^ .^' \s - +[" +]{.`i;I; ::."!??l.^Ywj}<, (n, ,~_:` .,, ` `` ' '... ^+-l,]}]}\\j/!. . ` 'I<~`'{tl..^` \s - '' .,<{[>" i/i" `-[; ,<_[>^i_l,:^_! ',+l.. ^,:,,. ;~>l;^ l> ',;I^???~,'l".. .. \s - ;{?l. !+ .. .<1i '^' "}|{:-+-;?\\[)-] ^:l1-:. '' '`, . ';.`~^ '. ..^`. \s - 'i+;]}!,. <))\\!<|ji >((_}}?t)}\\\\v|]?jI!), "lf!l. ... .^ . ". \s - .+{>` l/z\\!,>""I+~_){]vQjut_~~>>>_-<]<-)f":l_v){\\/1}}}{t/\\0?z~. ^' `-l . . .. . \s - l\\ :_>>i:^+\\)_-]!:>-+l'...`^;1!^ 'l>})l\\n\\Qt?]?]})1{][[(XC>^ ... - ^-+^.i-((?!"`:>l<[~<]nQY+?????][{\\cmO||l . ''.. - '|: '^[{~)\\_+++))1{uxnvt(t){{[[u0\\1|({1()){-?|xfc: .. \s - .<-, ]-]]]})11)){{{{}{{)|{}}{1{111{11{{}}]_!"x\\]Xf \s - ,]<\\}][[[[[[[[[[[[[[[]][[[[[[[[][[[[[[]?-+!YC{z} .` - "_[}?]][[[[[[[[[[[[[[[[[[][[[[[[[[[[[[[[[]vn\\?. ^ - ^. ;{_(_??][[[[[[[[[[[[[[[[[|[[[[[[[[[[[[[[[[v_(]^ \s - .' '. :t>/?[[[[[[[[[[[[[[[[[[[]t\\][[[[[[[[[[][]?u;()_ .. .'. . . - `. .` :)!j_]][[[]]]]][[[[[[[[[[}j(/{[[[[[][[}1{~n!)ft . .. .. . .. . - ". ..' .` . "' .^":;~ti{\\1]][]]]??]]]]][]]??[){[}[[[[?+}]!^':``^``,`;I.!<>?>:??i:;-;,<_..^,Ii: 'l,i+```' ..'!; ''. ?~.'<+ .li!:,1?}[[[[[[[[[[[[[[[[[[[[)1}]t[:, "O" . .' . ^!' . ". i+' . \s - ^. . .':`'`~/1,-<-~^'^'^^,`.."i_>^. `1t]!,^I]l^;`,I::_?]?[!:;`.`"'`l!l<1f{~>;]\\1(]I>~l!l[<,,;`lI,~},^>!>l'...'": 'x' ]l>i .:I1[~]]]]}}}}}[[}}[}}}}}[[]]]??1}}[}~;:>vx. :;..:??,.' ^` I;>.";:"^' .. ^'.^"" ' .!},' . - ~!;:!".":i"^_/|]^li(\\1;;it{' .[\\fft+<}(/{}/)|f||'.^{/[)?!:(?+-,I+fjtil"'"+fj{i:',!!;!!^:.`r. r; !l'"i1?!i>~+_?][[[?-???]][[]?-+-]??+~<_{[_l?> ''^l;l`-}<`^.i>+l ``;I":+?!~-l ,>>l.'.;. ':!!(/!":,I - /t1ffft+{jff/ttff)];)?1(/tt\\/t/tfttttfftf/1\\|t\\|/?<_]_]{<_]/f({fffjttf/[i>1//|tft|" :<~:+}, ]>if" .:-~ >) ^`^l)f(_{/\\}-+1\\()t-{j/]!:^'l<]\\)+ ."_?I_{ - ffft)|)(t[_-{tjjrjrj/{(||}(rjj\\1)I<\\((ffj/rjffttjffftrjfffrtfff/f[1jjffffftt//)}tttff/ttt[<{rj}tf1?<:~{/j)>)fttf|?)tfffftt1_;+tf1-1|~i1, >;:} '1_ ;( .. .. `:"_1{}tjtvj)vjr/|jfff/<(tf)+1/)1j)~~-[j[l|[(/\\j{:-]]([}\\t - """; - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java deleted file mode 100644 index e9a486d..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java +++ /dev/null @@ -1,54 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.cono.morny.util.FileUtils; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.net.URISyntaxException; -import java.security.NoSuchAlgorithmException; - -/** - * Morny Cono 的 Coeur 的程序属性存放类 - */ -public class MornySystem { - - /** - * 程序的语义化版本号
- * 会由 gradle 任务 {@code updateVersionCode} 更新 - */ - public static final String VERSION = GradleProjectConfigures.VERSION; - - /** - * Morny Coeur 当前的版本代号.
- * 一个单个单词,一般作为一个大版本的名称,只在重大更新改变
- * 格式保持为仅由小写字母和数字组成
- * 有时也可能是复合词或特殊的词句
- *
- * 会由 gradle 任务 {@code updateVersionCode} 更新 - */ - public static final String CODENAME = GradleProjectConfigures.CODENAME; - - /** - * 获取程序 jar 文件的 md5-hash 值
- *
- * 只支持 jar 文件方式启动的程序 —— - * 如果是通过 classpath 来启动,程序无法找到本体jar文件,则会返回 {@code } 文本 - *
- * 值格式为 {@link java.lang.String} - * - * @return 程序jar文件的 md5-hash 值字符串,或 {@code } 如果出现错误 - */ - @Nonnull - public static String getJarMd5() { - try { - return FileUtils.getMD5Three(MornyCoeur.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); - } catch (IOException | URISyntaxException e) { - e.printStackTrace(System.out); - return ""; - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(System.out); - return ""; - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java b/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java deleted file mode 100644 index ba93789..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java +++ /dev/null @@ -1,58 +0,0 @@ -package cc.sukazyo.cono.morny; - -import com.pengrad.telegrambot.model.ChatMember.Status; -import java.util.HashSet; -import java.util.Set; - -/** - * 对用户进行身份权限验证的管理类 - */ -public class MornyTrusted { - - /** - * 群聊id,其指向的群聊指示了哪个群的成员是受信任的 - * @see #isTrusted(long) 受信检查 - */ - public final Long TRUSTED_CHAT_ID; - - /** - * morny 的主人
- * 这项值的对象总是会被认为是可信任的 - */ - public final long MASTER; - - private final Set TRUSTED_READERS_OF_DINNER; - - public MornyTrusted (long master, long trustedChatId, Set trustedRDinner) { - this.TRUSTED_CHAT_ID = trustedChatId; - this.MASTER = master; - this.TRUSTED_READERS_OF_DINNER = new HashSet<>(){{ - this.add(master); - this.addAll(trustedRDinner); - }}; - } - - /** - * 用于检查一个 telegram-user 是否受信任
- *
- * 用户需要受信任才能执行一些对程序甚至是宿主环境而言危险的操作,例如关闭程序
- *
- * 它的逻辑(目前)是检查群聊 {@link #TRUSTED_CHAT_ID} 中这个用户是否为群组管理员 - * - * @param userId 需要检查的用户的id - * @return 所传递的用户id对应的用户是否受信任 - */ - public boolean isTrusted (long userId) { - if (userId == MASTER) return true; - return MornyCoeur.extra().isUserInGroup(userId, TRUSTED_CHAT_ID, Status.administrator); - } - - public boolean isTrustedForDinnerRead (long userId) { - return TRUSTED_READERS_OF_DINNER.contains(userId); - } - - public Set getTrustedReadersOfDinnerSet () { - return Set.copyOf(TRUSTED_READERS_OF_DINNER); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java deleted file mode 100644 index 17a831f..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ /dev/null @@ -1,249 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.cono.morny.util.CommonFormat; - -import javax.annotation.Nonnull; - -import java.util.HashSet; -import java.util.Set; - -import static cc.sukazyo.cono.morny.Log.logger; - -/** - * 程序启动入口
- *
- * 会处理程序传入的参数和选项等数据,并执行对应的启动方式
- * - * @since 0.4.0.0 - */ -public class ServerMain { - - public static final String PROP_TOKEN_KEY = "TELEGRAM_BOT_API_TOKEN"; - public static final String PROP_TOKEN_MORNY_KEY = "MORNY_TG_TOKEN"; - - private static final String THREAD_MORNY_INIT = "morny-init"; - - /** - * 程序入口,也是参数处理器
- *
- * 以 {@code -} 开头的参数会被解析为选项
- *
- * 支持以下选项 - *

- * 除去选项之外,第一个参数会被赋值为 bot 的 telegram bot api token, - * 第二个参数会被赋值为 bot 的 username 限定名。其余的参数会被认定为无法理解。
- * 自 {@code 0.4.2.3},token 和 username 的赋值已被选项组支持
- * 自 {@code 0.5.0.4},旧的直接通过参数为 bot token & username 赋值的方式已被删除 - * 使用参数所进行取值的 token 和 username 已被转移至 {@code --token} 和 {@code --username} 参数
- * - * @see MornyCoeur#main - * @since 0.4.0.0 - * @param args 参数组 - */ - public static void main (@Nonnull String[] args) { - - //# - //# 启动参数设置区块 - //# - - boolean versionEchoMode = false; - boolean welcomeEchoMode = false; - boolean showWelcome = true; - String key = null; - String username = null; - boolean outdatedBlock = false; - long master = 793274677L; - Set trustedReadersOfDinner = new HashSet<>(); - long trustedChat = -1001541451710L; - boolean autoCmdList = false; - boolean autoCmdRemove = false; - String api = null; - String api4File = null; - - for (int i = 0; i < args.length; i++) { - - if (args[i].startsWith("-")) { - - switch (args[i]) { - case "--outdated-block", "-ob" -> { - outdatedBlock = true; - continue; - } - case "--no-hello", "-hf", "--quiet", "-q" -> { - showWelcome = false; - continue; - } - case "--only-hello", "-ho", "-o", "-hi" -> { - welcomeEchoMode = true; - continue; - } - case "--version", "-v" -> { - versionEchoMode = true; - continue; - } - case "--token", "-t" -> { - i++; - key = args[i]; - continue; - } - case "--username", "-u" -> { - i++; - username = args[i]; - continue; - } - case "--master", "-mm" -> { - i++; - master = Long.parseLong(args[i]); - continue; - } - case "--trusted-chat", "-trs" -> { - i++; - trustedChat = Long.parseLong(args[i]); - continue; - } - //noinspection SpellCheckingInspection - case "--trusted-reader-dinner", "-trsd" -> { - i++; - trustedReadersOfDinner.add(Long.parseLong(args[i])); - continue; - } - case "--auto-cmd", "-cmd", "-c" -> { - autoCmdList = true; - autoCmdRemove = true; - continue; - } - case "--auto-cmd-list", "-ca" -> { - autoCmdList = true; - continue; - } - case "--auto-cmd-remove", "-cr" -> { - autoCmdRemove = true; - continue; - } - case "--api", "-a" -> { - i++; - api = args[i]; - continue; - } - case "--api-files", "files-api", "-af" -> { - i++; - api4File = args[i]; - continue; - } - } - - } - - logger.warn("Can't understand arg to some meaning :\n " + args[i]); - - } - - String propToken = null; - String propTokenKey = null; - for (String iKey : new String[]{PROP_TOKEN_KEY, PROP_TOKEN_MORNY_KEY}) { - if (System.getenv(iKey) != null) { - propToken = System.getenv(iKey); - propTokenKey = iKey; - } - } - - //# - //# 启动相关参数的检查和处理 - //# - - if (versionEchoMode) { - - logger.info(String.format(""" - Morny Cono Version - - version : - %s %s - - md5hash : - %s - - co.time : - %d - %s [UTC]""", - MornySystem.VERSION, MornySystem.CODENAME.toUpperCase(), - MornySystem.getJarMd5(), - GradleProjectConfigures.COMPILE_TIMESTAMP, - CommonFormat.formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0) - )); - return; - - } - - if (showWelcome) logger.info(MornyHello.MORNY_PREVIEW_IMAGE_ASCII); - if (welcomeEchoMode) return; - - logger.info(String.format(""" - ServerMain.java Loaded >>> - - version %s (%s)(%d) - - Morny %s""", - MornySystem.VERSION, - MornySystem.getJarMd5(), GradleProjectConfigures.COMPILE_TIMESTAMP, - MornySystem.CODENAME.toUpperCase() - )); - - //# - //# Coeur 参数检查和正式启动主程序 - //# - - if (propToken != null) { - key = propToken; - logger.info("Parameter set by EnvVar $"+propTokenKey); - } - if (key == null) { - logger.info("Parameter required has no value:\n --token."); - return; - } - Thread.currentThread().setName(THREAD_MORNY_INIT); - MornyCoeur.main( - api, api4File, - key, username, - master, trustedChat, trustedReadersOfDinner, - outdatedBlock?System.currentTimeMillis():0, - autoCmdList, autoCmdRemove - ); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListener.java b/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListener.java deleted file mode 100644 index a7c8c5e..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListener.java +++ /dev/null @@ -1,66 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api; - -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -@SuppressWarnings("unused") -public abstract class EventListener { - - public boolean onMessage (@Nonnull Update update) { - return false; - } - - public boolean onEditedMessage (@Nonnull Update update) { - return false; - } - - public boolean onChannelPost (@Nonnull Update update) { - return false; - } - - public boolean onEditedChannelPost (@Nonnull Update update) { - return false; - } - - public boolean onInlineQuery (@Nonnull Update update) { - return false; - } - - public boolean onChosenInlineResult (@Nonnull Update update) { - return false; - } - - public boolean onCallbackQuery (@Nonnull Update update) { - return false; - } - - public boolean onShippingQuery (@Nonnull Update update) { - return false; - } - - public boolean onPreCheckoutQuery (@Nonnull Update update) { - return false; - } - - public boolean onPoll (@Nonnull Update update) { - return false; - } - - public boolean onPollAnswer (@Nonnull Update update) { - return false; - } - - public boolean onMyChatMemberUpdated (@Nonnull Update update) { - return false; - } - - public boolean onChatMemberUpdated (@Nonnull Update update) { - return false; - } - - public boolean onChatJoinRequest (@Nonnull Update update) { - return false; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java b/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java deleted file mode 100644 index 18d0004..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java +++ /dev/null @@ -1,123 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api; - -import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; -import com.google.gson.GsonBuilder; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Function; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class EventListenerManager { - - private static final List listeners = new ArrayList<>(); - - private static class EventPublisher extends Thread { - - private final Function exec; - - public EventPublisher(@Nonnull Update update, @Nonnull Function exec) { - this.setName("EVT"+update.updateId()); - this.exec = exec; - } - - @Override - public void run () { - for (EventListener x : listeners) { - try { - - if (exec.apply(x)) return; - - } catch (EventRuntimeException e) { - - final StringBuilder errorMessage = new StringBuilder(); - errorMessage.append("Event runtime breaks: " + e.getMessage()).append('\n'); - errorMessage.append("at " + e.getStackTrace()[0].toString()).append('\n'); - errorMessage.append("at " + e.getStackTrace()[1].toString()).append('\n'); - errorMessage.append("at " + e.getStackTrace()[2].toString()).append('\n'); - errorMessage.append("at " + e.getStackTrace()[3].toString()).append('\n'); - if (e instanceof EventRuntimeException.ActionFailed) { - errorMessage.append(( - "\"telegram request track\": " + - new GsonBuilder().setPrettyPrinting().create().toJson(((EventRuntimeException.ActionFailed)e).getResponse()) - ).indent(4)).append('\n'); - } - - logger.error(errorMessage.toString()); - - } catch (Exception e) { - - logger.error("Event Error!"); - e.printStackTrace(System.out); - - } - } - } - - } - - public static void addListener (@Nonnull EventListener... listeners) { - EventListenerManager.listeners.addAll(Arrays.asList(listeners)); - } - - public static void publishMessageEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onMessage(update)).start(); - } - - public static void publishEditedMessageEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onEditedMessage(update)).start(); - } - - public static void publishChannelPostEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onChannelPost(update)).start(); - } - - public static void publishEditedChannelPostEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onEditedChannelPost(update)).start(); - } - - public static void publishInlineQueryEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onInlineQuery(update)).start(); - } - - public static void publishChosenInlineResultEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onChosenInlineResult(update)).start(); - } - - public static void publishCallbackQueryEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onCallbackQuery(update)).start(); - } - - public static void publishShippingQueryEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onShippingQuery(update)).start(); - } - - public static void publishPreCheckoutQueryEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onPreCheckoutQuery(update)).start(); - } - - public static void publishPollEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onPoll(update)).start(); - } - - public static void publishPollAnswerEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onPollAnswer(update)).start(); - } - - public static void publishMyChatMemberUpdatedEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onMyChatMemberUpdated(update)).start(); - } - - public static void publishChatMemberUpdatedEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onChatMemberUpdated(update)).start(); - } - - public static void publishChatJoinRequestEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onChatJoinRequest(update)).start(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java b/src/main/java/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java deleted file mode 100644 index 43f0376..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java +++ /dev/null @@ -1,36 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api; - -import com.pengrad.telegrambot.model.request.InlineQueryResult; - -public class InlineQueryUnit> { - - public static final int DEFAULT_INLINE_CACHE_TIME = 300; - public static final boolean DEFAULT_INLINE_PERSONAL_RESP = false; - - private int cacheTime = DEFAULT_INLINE_CACHE_TIME; - private boolean isPersonal = DEFAULT_INLINE_PERSONAL_RESP; - public final T result; - - public InlineQueryUnit (T result) { - this.result = result; - } - - public int cacheTime () { - return cacheTime; - } - - public InlineQueryUnit cacheTime (int cacheTime) { - this.cacheTime = cacheTime; - return this; - } - - public boolean isPersonal () { - return isPersonal; - } - - public InlineQueryUnit isPersonal (boolean isPersonal) { - this.isPersonal = isPersonal; - return this; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/OnUpdate.java b/src/main/java/cc/sukazyo/cono/morny/bot/api/OnUpdate.java deleted file mode 100644 index e03653b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/api/OnUpdate.java +++ /dev/null @@ -1,59 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api; - -import com.pengrad.telegrambot.UpdatesListener; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import java.util.List; - -public class OnUpdate { - - public static int onNormalUpdate (@Nonnull List updates) { - for (Update update : updates) { - if (update.message() != null) { - EventListenerManager.publishMessageEvent(update); - } - if (update.editedMessage() != null) { - EventListenerManager.publishEditedMessageEvent(update); - } - if (update.channelPost() != null) { - EventListenerManager.publishChannelPostEvent(update); - } - if (update.editedChannelPost() != null) { - EventListenerManager.publishEditedChannelPostEvent(update); - } - if (update.inlineQuery() != null) { - EventListenerManager.publishInlineQueryEvent(update); - } - if (update.chosenInlineResult() != null) { - EventListenerManager.publishChosenInlineResultEvent(update); - } - if (update.callbackQuery() != null) { - EventListenerManager.publishCallbackQueryEvent(update); - } - if (update.shippingQuery() != null) { - EventListenerManager.publishShippingQueryEvent(update); - } - if (update.preCheckoutQuery() != null) { - EventListenerManager.publishPreCheckoutQueryEvent(update); - } - if (update.poll() != null) { - EventListenerManager.publishPollEvent(update); - } - if (update.pollAnswer() != null) { - EventListenerManager.publishPollAnswerEvent(update); - } - if (update.myChatMember() != null) { - EventListenerManager.publishMyChatMemberUpdatedEvent(update); - } - if (update.chatMember() != null) { - EventListenerManager.publishChatMemberUpdatedEvent(update); - } - if (update.chatJoinRequest() != null) { - EventListenerManager.publishChatJoinRequestEvent(update); - } - } - return UpdatesListener.CONFIRMED_UPDATES_ALL; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.java deleted file mode 100644 index 54b850a..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.java +++ /dev/null @@ -1,58 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.DeleteMessage; -import com.pengrad.telegrambot.request.GetChatMember; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class DirectMsgClear implements ISimpleCommand { - - @Nonnull @Override public String getName () { return "r"; } - - @Nullable @Override public String[] getAliases () { return new String[0]; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - logger.debug("Executing command /r"); - if (event.message().replyToMessage() == null) return; - logger.trace("Message is a reply"); - if (event.message().replyToMessage().from().id() != MornyCoeur.getUserid()) return; - logger.trace("Message is from me"); - if (System.currentTimeMillis()/1000 - event.message().replyToMessage().date() > 48*60*60) return; - logger.trace("Message is not older than 48 hours"); - - final boolean isTrusted = MornyCoeur.trustedInstance().isTrusted(event.message().from().id()); - - if ( - isTrusted || ( - event.message().replyToMessage().replyToMessage() != null && - event.message().replyToMessage().replyToMessage().from().id().equals(event.message().from().id()) - ) - ) { - - MornyCoeur.extra().exec(new DeleteMessage( - event.message().chat().id(), event.message().replyToMessage().messageId() - )); - if (event.message().chat().type() == Chat.Type.Private || ( - MornyCoeur.extra().exec( - new GetChatMember(event.message().chat().id(), event.message().from().id()) - ).chatMember().canDeleteMessages() - )) { - MornyCoeur.extra().exec(new DeleteMessage( - event.message().chat().id(), event.message().messageId() - )); - } - - } else logger.trace("User is not trusted"); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java deleted file mode 100644 index e770b9f..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java +++ /dev/null @@ -1,205 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.CommonConvert; -import cc.sukazyo.cono.morny.util.CommonEncrypt; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape; -import com.pengrad.telegrambot.model.PhotoSize; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.GetFile; -import com.pengrad.telegrambot.request.SendDocument; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; -import java.util.Base64; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class Encryptor implements ITelegramCommand { - - @Nonnull @Override public String getName () { return "encrypt"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[algorithm|(l)] [(uppercase)]"; } - @Nonnull @Override public String getDescription () { return "通过指定算法加密回复的内容 (目前只支持文本)"; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - // show a simple help page - // the first paragraph lists available encrypt algorithms, and its aliases. - // with the separator "---", - // the second paragraphs shows the mods available and its aliases. - if (!command.hasArgs() || (command.getArgs()[0].equals("l") && command.getArgs().length==1)) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), """ - base64, b64 - base64url, base64u, b64u - base64decode, base64d, b64d - base64url-decode, base64ud, b64ud - sha1 - sha256 - sha512 - md5 - --- - uppercase, upper, u (sha1/sha256/sha512/md5 only) - """ - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - return; - } - - // param1 is the encrypting algorithm, it MUST EXIST. - // so the mod will be set in param2. - // and for now only support UPPERCASE mod, so it exists in param2, or there should no any params. - boolean modUpperCase = false; - if (command.getArgs().length > 1) { - if (command.getArgs().length < 3 && ( - command.getArgs()[1].equalsIgnoreCase("uppercase") || - command.getArgs()[1].equalsIgnoreCase("u") || - command.getArgs()[1].equalsIgnoreCase("upper") - )) { - modUpperCase = true; - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - return; - } - } - - // for now, only support reply to A TEXT MESSAGE or ONE UNIVERSAL FILE - // if the replied message contains a UNIVERSAL FILE, it will use the file and will not use the text with it - // do not support TELEGRAM INLINE IMAGE/VIDEO/AUDIO yet - // do not support MULTI_FILE yet - // if there's no text message in reply, it will report null as result. - boolean inputText; - byte[] data; - String dataName; - if (event.message().replyToMessage() != null && event.message().replyToMessage().document() != null) { - inputText = false; - try { - data = MornyCoeur.getAccount().getFileContent(MornyCoeur.extra().exec(new GetFile( - event.message().replyToMessage().document().fileId() - )).file()); - } catch (IOException e) { - logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage()); - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_NETWORK_ERR - ).replyToMessageId(event.message().messageId())); - return; - } - dataName = event.message().replyToMessage().document().fileName(); - } else if (event.message().replyToMessage() != null && event.message().replyToMessage().photo() != null) { - inputText = false; - try { - PhotoSize originPhoto = null; - long photoSize = 0; - for (PhotoSize size : event.message().replyToMessage().photo()) if (photoSize < (long)size.width() *size.height()) { - originPhoto = size; - photoSize = (long)size.width() *size.height(); - } // found max size (original) image in available sizes - if (originPhoto==null) throw new IOException("no photo object from api."); - data = MornyCoeur.getAccount().getFileContent(MornyCoeur.extra().exec(new GetFile( - originPhoto.fileId() - )).file()); - } catch (IOException e) { - logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage()); - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_NETWORK_ERR - ).replyToMessageId(event.message().messageId())); - return; - } - dataName = "photo"+CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(String.valueOf(System.currentTimeMillis()))).substring(32-12).toUpperCase()+".png"; - } else if (event.message().replyToMessage() != null && event.message().replyToMessage().text() != null) { - inputText = true; - data = event.message().replyToMessage().text().getBytes(CommonEncrypt.ENCRYPT_STANDARD_CHARSET); - dataName = null; - } else { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "null" - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - return; - } - - boolean echoString = true; - String resultString = null; - byte[] result = null; - String resultName = null; - switch (command.getArgs()[0]) { - case "base64", "b64", "base64url", "base64u", "b64u" -> { - final Base64.Encoder b64tool = command.getArgs()[0].contains("u") ? Base64.getUrlEncoder() : Base64.getEncoder(); - result = b64tool.encode(data); - if (!inputText) { - echoString = false; - resultName = dataName+".b64.txt"; - } else { - resultString = new String(result, CommonEncrypt.ENCRYPT_STANDARD_CHARSET); - } - } - case "base64decode", "base64d", "b64d", "base64url-decode", "base64ud", "b64ud" -> { - final Base64.Decoder b64tool = command.getArgs()[0].contains("u") ? Base64.getUrlDecoder() : Base64.getDecoder(); - try { result = b64tool.decode(data); } - catch (IllegalArgumentException e) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - return; - } - if (!inputText) { - echoString = false; - resultName = CommonEncrypt.base64FilenameLint(dataName); - } else { - resultString = new String(result, CommonEncrypt.ENCRYPT_STANDARD_CHARSET); - } - } - case "md5" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(data)); - case "sha1" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha1(data)); - case "sha256" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha256(data)); - case "sha512" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha512(data)); - default -> { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - return; - } - } - if (modUpperCase) { - // modUpperCase support only algorithm that showed as HEX value. - // it means md5, sha1, sha256, sha512 here. - // other will report wrong param. - switch (command.getArgs()[0]) { - case "md5", "sha1", "sha256", "sha512" -> { - assert resultString != null; - resultString = resultString.toUpperCase(); - } - default -> { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - return; - } - } - } - if (echoString) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "
" + MsgEscape.escapeHtml(resultString) + "
" - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - } else { - MornyCoeur.extra().exec(new SendDocument( - event.message().chat().id(), - result - ).fileName(resultName).replyToMessageId(event.message().messageId())); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java deleted file mode 100644 index ddbe9e2..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java +++ /dev/null @@ -1,92 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.MornyTrusted; -import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle; -import cc.sukazyo.cono.morny.data.TelegramStickers; - -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * {@link OnEventHackHandle} 的命令行前端 - * @since 0.4.2.0 - */ -public class EventHack implements ITelegramCommand { - - @Nonnull @Override public String getName () { return "event_hack"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return "[(user|group|any)]"; } - @Nonnull @Override public String getDescription () { return "输出 bot 下一个获取到的事件序列化数据"; } - - /** - * {@link OnEventHackHandle} 的命令行前端
- *
- * 实现了通过命令行进行 EventHack 功能。
- * 支持三种模式,默认为 {@link OnEventHackHandle.HackType#USER USER}, - * {@link OnEventHackHandle.HackType#ANY ANY} 时,将会通过 {@link MornyTrusted#isTrusted(long)} 检查触发用户的权限 - * - * @param event 命令基础参数,触发的事件对象本身 - * @param command 命令基础参数,解析出的命令对象 - * @since 0.4.2.0 - */ - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - boolean isOk = false; - - String x_mode = ""; - if (command.hasArgs()) { - x_mode = command.getArgs()[0]; - } - - switch (x_mode) { - case "any": - if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) { - OnEventHackHandle.registerHack( - event.message().messageId(), - event.message().from().id(), - event.message().chat().id(), - OnEventHackHandle.HackType.ANY - );isOk = true; - } - break; - case "group": - OnEventHackHandle.registerHack( - event.message().messageId(), - event.message().from().id(), - event.message().chat().id(), - OnEventHackHandle.HackType.GROUP - );isOk = true; - break; - default: - OnEventHackHandle.registerHack( - event.message().messageId(), - event.message().from().id(), - event.message().chat().id(), - OnEventHackHandle.HackType.USER - );isOk = true; - break; - } - - if (isOk) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_WAITING - ).replyToMessageId(event.message().messageId()) - ); - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_403 - ).replyToMessageId(event.message().messageId()) - ); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java deleted file mode 100644 index bcc2bcf..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java +++ /dev/null @@ -1,79 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.User; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.GetChatMember; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.response.GetChatMemberResponse; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class GetUsernameAndId implements ITelegramCommand { - - @Nonnull @Override public String getName () { return "user"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return "[userid]"; } - @Nonnull @Override public String getDescription () { return "获取指定或回复的用户相关信息"; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - final String[] args = command.getArgs(); - - if (args.length > 1) { MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] Too much arguments." - ).replyToMessageId(event.message().messageId())); return; } - - long userId = event.message().from().id(); - - if (event.message().replyToMessage()!= null) { - userId = event.message().replyToMessage().from().id(); - } - if (args.length > 0) { - try { - userId = Long.parseLong(args[0]); - } catch (NumberFormatException e) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] " + e.getMessage() - ).replyToMessageId(event.message().messageId())); - return; - } - } - - final GetChatMemberResponse response = MornyCoeur.getAccount().execute( - new GetChatMember(event.message().chat().id(), userId) - ); - - if (response.chatMember() == null) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] user not found." - ).replyToMessageId(event.message().messageId())); - return; - } - - final User user = response.chatMember().user(); - - if (user.id() == 136817688) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "$__channel_identify" - )); - return; - } - - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - TelegramUserInformation.informationOutputHTML(user) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java deleted file mode 100644 index 453a97e..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java +++ /dev/null @@ -1,19 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public interface ISimpleCommand { - - @Nonnull - String getName(); - - @Nullable - String[] getAliases(); - - void execute (@Nonnull InputCommand command, @Nonnull Update event); - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java deleted file mode 100644 index b090a95..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import javax.annotation.Nonnull; - -public interface ITelegramCommand extends ISimpleCommand { - - @Nonnull - String getParamRule(); - @Nonnull - String getDescription(); - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java deleted file mode 100644 index 212305a..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java +++ /dev/null @@ -1,86 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.data.ip186.IP186QueryResponse; -import cc.sukazyo.cono.morny.data.ip186.IP186QueryHandler; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; -import org.jetbrains.annotations.NotNull; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - - -/** - * {@value IP186QueryHandler#SITE_URL} 查询的 telegram 命令前端 - * @since 0.4.2.10 - */ -public class Ip186Query { - - public static final String CMD_IP = "ip"; - public static final String CMD_WHOIS = "whois"; - - public static class Ip implements ITelegramCommand { - @Nonnull @Override public String getName () { return CMD_IP; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[ip]"; } - @Nonnull @Override public String getDescription () { return "通过 https://ip.186526.xyz 查询 ip 资料"; } - @Override public void execute (@NotNull InputCommand command, @NotNull Update event) { exec(event, command); } - } - - public static class Whois implements ITelegramCommand { - @Nonnull @Override public String getName () { return CMD_WHOIS; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[domain]"; } - @Nonnull @Override public String getDescription () { return "通过 https://ip.186526.xyz 查询域名资料"; } - @Override public void execute (@NotNull InputCommand command, @NotNull Update event) { exec(event, command); } - } - - private static void exec (@Nonnull Update event, @Nonnull InputCommand command) { - - String arg = null; - if (!command.hasArgs()) { - if (event.message().replyToMessage() != null) { - arg = event.message().replyToMessage().text(); - } - } else if (command.getArgs().length > 1) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] Too much arguments." - ).replyToMessageId(event.message().messageId())); - return; - } else { - arg = command.getArgs()[0]; - } - if (arg == null) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] No ip defined." - ).replyToMessageId(event.message().messageId())); - return; - } - - try { - IP186QueryResponse response = switch (command.getCommand()) { - case CMD_IP -> IP186QueryHandler.queryIp(arg); - case CMD_WHOIS -> IP186QueryHandler.queryWhoisPretty(arg); - default -> throw new IllegalArgumentException("Unknown 186-IP query method " + command.getCommand()); - }; - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - escapeHtml(response.url()) + "\n" + escapeHtml(response.body()) + "" - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - } catch (Exception e) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Exception] in query:\n" + escapeHtml(e.getMessage()) + "" - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java deleted file mode 100644 index 7b37258..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ /dev/null @@ -1,373 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.GradleProjectConfigures; -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.MornySystem; -import cc.sukazyo.cono.morny.data.MornyJrrp; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; -import com.pengrad.telegrambot.model.BotCommand; -import com.pengrad.telegrambot.model.DeleteMyCommands; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; -import com.pengrad.telegrambot.request.SetMyCommands; -import org.jetbrains.annotations.NotNull; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static cc.sukazyo.cono.morny.Log.logger; -import static cc.sukazyo.cono.morny.util.CommonFormat.formatDate; -import static cc.sukazyo.cono.morny.util.CommonFormat.formatDuration; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class MornyCommands { - - private final Map commands = new LinkedHashMap<>(); - - private void pushCommandTo (@Nonnull String name, @Nonnull ISimpleCommand instance) { - if (commands.containsKey(name)) { - logger.warn(String.format(""" - Telegram command instance named "%s" already exists and will be override by another command instance - - current: %s - - new : %s""", - name, - commands.get(name).getClass().getName(), - instance.getClass().getName() - )); - } - commands.put(name, instance); - } - - public void register (@Nonnull ISimpleCommand... list) { - for (ISimpleCommand instance : list) { - final String[] aliases = instance.getAliases(); - pushCommandTo(instance.getName(), instance); - if (aliases!=null) for (String alias : aliases) pushCommandTo(alias, instance); - } - } - - @SuppressWarnings("NonAsciiCharacters") - public MornyCommands () { - - register( - new ON(), - new Hello(), new HelloOnStart(), - new GetUsernameAndId(), - new EventHack(), - new Nbnhhsh(), - new Ip186Query.Ip(), - new Ip186Query.Whois(), - new Encryptor(), - new SaveData(), - new MornyInformations(), - new Version(), - new MornyRuntime(), - new Jrrp(), - new Exit(), new ExitAlias() - ); - - // 特殊的命令 - register( - new Testing(), - new DirectMsgClear() - ); - - // 统一注册这些奇怪的东西&.& - register( - new 喵呜.抱抱(), - new 喵呜.揉揉(), - new 喵呜.蹭蹭(), - new 喵呜.贴贴(), - new 私わね(), - new 喵呜.Progynova() - ); - - } - - public boolean execute (@Nonnull InputCommand command, @Nonnull Update event) { - if (commands.containsKey(command.getCommand())) { - commands.get(command.getCommand()).execute(command, event); - return true; - } - return nonCommandExecutable(event, command); - } - - public void automaticUpdateList () { - BotCommand[] commandList = getCommandListTelegram(); - automaticRemoveList(); - MornyCoeur.extra().exec(new SetMyCommands( - commandList - )); - logger.info("automatic updated telegram command list :\n" + commandListToString(commandList)); - } - - public void automaticRemoveList () { - MornyCoeur.extra().exec(new DeleteMyCommands()); - logger.info("cleaned up command list."); - } - - private String commandListToString (@Nonnull BotCommand[] list) { - StringBuilder builder = new StringBuilder(); - for (BotCommand signal : list) { - builder.append(signal.command()).append(" - ").append(signal.description()).append("\n"); - } - return builder.substring(0, builder.length()-1); - } - - public BotCommand[] getCommandListTelegram () { - final List telegramFormatListing = new ArrayList<>(); - commands.forEach((regKey, command) -> { - if (command instanceof ITelegramCommand && regKey.equals(command.getName())) { - telegramFormatListing.add(formatTelegramCommandListLine( - command.getName(), - ((ITelegramCommand)command).getParamRule(), - ((ITelegramCommand)command).getDescription() - )); - if (command.getAliases() != null) for (String alias : command.getAliases()) { - telegramFormatListing.add(formatTelegramCommandListLine(alias, "", "↑")); - } - } - }); - return telegramFormatListing.toArray(BotCommand[]::new); - } - - private BotCommand formatTelegramCommandListLine (@Nonnull String commandName, @Nonnull String paramRule, @Nonnull String intro) { - return new BotCommand(commandName, "".equals(paramRule) ? (intro) : (paramRule+" - "+intro)); - } - - private boolean nonCommandExecutable (Update event, InputCommand command) { - if (command.getTarget() == null) return false; // 无法解析的命令,转交事件链后代处理 - else { // 无法解析的显式命令格式,报错找不到命令 - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId()) - ); - return true; - } - } - - /// /// /// /// /// /// /// /// /// - /// - /// Old Simple Command Block - /// - - private static class ON implements ITelegramCommand { - @Nonnull @Override public String getName () { return "o"; } - @Nullable - @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "检查是否在线"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandOnExec(event); } - } - private static void onCommandOnExec (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_ONLINE_STATUS_RETURN - ).replyToMessageId(event.message().messageId()) - ); - } - - private static class Hello implements ITelegramCommand { - @Nonnull @Override public String getName () { return "hello"; } - @Nullable @Override public String[] getAliases () { return new String[]{"hi"}; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "打招呼"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandHelloExec(event); } - } - private static class HelloOnStart implements ISimpleCommand { @Nonnull @Override public String getName () { return "start"; }@Nullable @Override public String[] getAliases () { return new String[0]; }@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandHelloExec(event); }} - private static void onCommandHelloExec (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_HELLO - ).replyToMessageId(event.message().messageId()) - ); - } - - private static class Exit implements ITelegramCommand { - @Nonnull @Override public String getName () { return "exit"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "关闭 Bot (仅可信成员)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandExitExec(event); } - } - private static class ExitAlias implements ISimpleCommand { - @Nonnull @Override public String getName () { return "quit"; } - @Nullable @Override public String[] getAliases () { return new String[]{"stop"}; } - @Override public void execute (@NotNull InputCommand command, @NotNull Update event) { onCommandExitExec(event); } - } - private static void onCommandExitExec (@Nonnull Update event) { - if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_EXIT - ).replyToMessageId(event.message().messageId()) - ); - logger.info("Morny exited by user " + TGToString.as(event.message().from()).toStringLogTag()); - System.exit(0); - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_403 - ).replyToMessageId(event.message().messageId()) - ); - logger.info("403 exited tag from user " + TGToString.as(event.message().from()).toStringLogTag()); - } - } - - private static class Version implements ITelegramCommand { - @Nonnull @Override public String getName () { return "version"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "检查 Bot 版本信息"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandVersionExec(event); } - } - private static void onCommandVersionExec (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format( - """ - version: - - Morny %s - - %s - core md5_hash: - - %s - compile timestamp: - - %d - - %s [UTC]""", - escapeHtml(MornySystem.CODENAME.toUpperCase()), - escapeHtml(MornySystem.VERSION), - escapeHtml(MornySystem.getJarMd5()), - GradleProjectConfigures.COMPILE_TIMESTAMP, - escapeHtml(formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0)) - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - } - - private static class MornyRuntime implements ITelegramCommand { - @Nonnull @Override public String getName () { return "runtime"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandRuntimeExec(event); } - } - /** - * @since 0.4.1.2 - */ - private static void onCommandRuntimeExec (@Nonnull Update event) { - String hostname; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - hostname = ""; - } - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format(""" - system: - - %s - - %s - - %s - java runtime: - - %s - - %s - vm memory: - - %d / %d MB - - %d cores - coeur version: - - %s (%s) - - %s - - %s [UTC] - - [%d] - continuous: - - %s - - [%d] - - %s [UTC] - - [%d]""", - // system - escapeHtml(hostname), - escapeHtml(String.format("%s (%s)", System.getProperty("os.name"), System.getProperty("os.arch"))), - escapeHtml(System.getProperty("os.version")), - // java - escapeHtml(System.getProperty("java.vm.vendor")+"."+System.getProperty("java.vm.name")), - escapeHtml(System.getProperty("java.vm.version")), - // memory - Runtime.getRuntime().totalMemory() / 1024 / 1024, - Runtime.getRuntime().maxMemory() / 1024 / 1024, - Runtime.getRuntime().availableProcessors(), - // version - escapeHtml(MornySystem.VERSION), - escapeHtml(MornySystem.CODENAME), - escapeHtml(MornySystem.getJarMd5()), - escapeHtml(formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0)), - GradleProjectConfigures.COMPILE_TIMESTAMP, - // continuous - escapeHtml(formatDuration(System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp)), - System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp, - escapeHtml(formatDate(MornyCoeur.coeurStartTimestamp, 0)), - MornyCoeur.coeurStartTimestamp - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - } - - private static class Jrrp implements ITelegramCommand { - @Nonnull @Override public String getName () { return "jrrp"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "获取 (假的) jrrp"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandJrrpExec(event); } - } - private static void onCommandJrrpExec (Update event) { - final double jrrp = MornyJrrp.getJrrpFromTelegramUser(event.message().from(), System.currentTimeMillis()); - final String endChar = jrrp>70 ? "!" : jrrp>30 ? ";" : "..."; - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format( - "%s 在(utc的)今天的运气指数是———— %.2f%% %s", - TGToString.as(event.message().from()).fullnameRefHtml(), - jrrp, escapeHtml(endChar) - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - } - - private static class SaveData implements ITelegramCommand { - @Nonnull @Override public String getName () { return "save"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "保存缓存数据到文件(仅可信成员)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onSaveDataExec(event); } - } - /** - * @since 0.4.3.0 - */ - private static void onSaveDataExec (Update event) { - if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) { - logger.info("called save from command by " + TGToString.as(event.message().from()).toStringLogTag()); - MornyCoeur.callSaveData(); - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_SAVED - ).replyToMessageId(event.message().messageId()) - ); - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_403 - ).replyToMessageId(event.message().messageId()) - ); - logger.info("403 call save tag from user " + TGToString.as(event.message().from()).toStringLogTag()); - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java deleted file mode 100644 index 9d815bb..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java +++ /dev/null @@ -1,46 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class MornyInformations implements ITelegramCommand { - - private static final String ACT_STICKER = "stickers"; - - @Nonnull @Override public String getName () { return "info"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[(stickers)|(stickers.)sticker_id]"; } - @Nonnull @Override public String getDescription () { return "输出 Morny 当前版本的一些预定义信息"; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - if (!command.hasArgs() || command.getArgs().length > 1) { - MornyCoeur.extra().exec(new SendSticker(event.message().chat().id(), TelegramStickers.ID_404).replyToMessageId(event.message().messageId())); - } - - final String action = command.getArgs()[0]; - - if (action.startsWith("stickers")) { - if (action.equals("stickers")) - TelegramStickers.echoAllStickers(MornyCoeur.extra(), event.message().chat().id(), event.message().messageId()); - else { - TelegramStickers.echoStickerByID( - action.substring((ACT_STICKER+".").length()), - MornyCoeur.extra(), event.message().chat().id(), event.message().messageId() - ); - } - return; - } - - MornyCoeur.extra().exec(new SendSticker(event.message().chat().id(), TelegramStickers.ID_404).replyToMessageId(event.message().messageId())); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java deleted file mode 100644 index 22e87cc..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java +++ /dev/null @@ -1,70 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.data.NbnhhshQuery; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class Nbnhhsh implements ITelegramCommand { - - @Nonnull @Override public String getName () { return "nbnhhsh"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return "[text]"; } - @Nonnull @Override public String getDescription () { return "检索文本内 nbnhhsh 词条"; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - try { - - String queryTarget = ""; - if (event.message().replyToMessage() != null && event.message().replyToMessage().text() != null) - queryTarget = event.message().replyToMessage().text(); - if (command.hasArgs()) - queryTarget = stringsConnecting(command.getArgs(), " ", 0, command.getArgs().length-1); - - NbnhhshQuery.GuessResult response = NbnhhshQuery.sendGuess(queryTarget); - - StringBuilder message = new StringBuilder("## Result of nbnhhsh query :"); - - for (NbnhhshQuery.Word word : response.words) { - if (word.trans != null && word.trans.length == 0) word.trans = null; - if (word.inputting != null && word.inputting.length == 0) word.inputting = null; - if (word.trans == null && word.inputting == null) continue; - message.append("\n\n[[ ").append(escapeHtml(word.name)).append(" ]]"); - if (word.trans != null) for (String trans : word.trans) { - message.append("\n* ").append(escapeHtml(trans)).append(""); - } - if (word.inputting != null) { - if (word.trans != null) message.append("\n"); - message.append(" maybe:"); - for (String trans : word.inputting) { - message.append("\n` ").append(escapeHtml(trans)).append(""); - } - } - } - - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - message.toString() - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - - } catch (Exception e) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Exception] in query:\n" + escapeHtml(e.getMessage()) + "" - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Roll.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Roll.java deleted file mode 100644 index c3c865f..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Roll.java +++ /dev/null @@ -1,4 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -public class Roll { -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java deleted file mode 100644 index d1bfb80..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java +++ /dev/null @@ -1,36 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class Testing implements ISimpleCommand { - - @Nonnull - @Override - public String getName () { - return "test"; - } - - @Nullable - @Override - public String[] getAliases () { - return null; - } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "Just a TEST command." - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java deleted file mode 100644 index 2bdbb7d..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java +++ /dev/null @@ -1,74 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -@SuppressWarnings("NonAsciiCharacters") -public class 喵呜 { - - public static class 抱抱 implements ISimpleCommand { - @Nonnull @Override public String getName () { return "抱抱"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "抱抱——" - )); - } - } - - public static class 揉揉 implements ISimpleCommand { - @Nonnull @Override public String getName () { return "揉揉"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "蹭蹭w" - )); - } - } - - public static class 蹭蹭 implements ISimpleCommand { - @Nonnull @Override public String getName () { return "蹭蹭"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "喵呜~-" - )); - } - } - - public static class 贴贴 implements ISimpleCommand { - @Nonnull @Override public String getName () { return "贴贴"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "(贴贴喵呜&.&)" - ).parseMode(ParseMode.HTML)); - } - } - - public static class Progynova implements ITelegramCommand { - @Nonnull @Override public String getName () { return "install"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "抽取一个神秘盒子"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_PROGYNOVA - ).replyToMessageId(event.message().messageId())); - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java deleted file mode 100644 index 0c92c84..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java +++ /dev/null @@ -1,38 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.concurrent.ThreadLocalRandom; - -@SuppressWarnings("NonAsciiCharacters") -public class 私わね implements ISimpleCommand { - - @Nonnull - @Override public String getName () { return "me"; } - - @Nullable - @Override public String[] getAliases () { return null; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - if (ThreadLocalRandom.current().nextInt(521) == 0) { - // 可以接入未来的心情系统(如果有的话) - final String text = switch (ThreadLocalRandom.current().nextInt(11)) { - case 0,7,8,9,10 -> "才不是"; - case 1,2,3,6 -> "才不是!"; - case 4,5 -> "才不是.."; - default -> throw new IllegalStateException("Unexpected random value in 私わね command."); - }; - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - text - ).replyToMessageId(event.message().messageId())); - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java deleted file mode 100644 index 7b165f9..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java +++ /dev/null @@ -1,38 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.bot.api.EventListenerManager; - -public class EventListeners { - - public static final OnTelegramCommand COMMANDS_LISTENER = new OnTelegramCommand(); - @SuppressWarnings("unused") public static final OnActivityRecord ACTIVITY_RECORDER = new OnActivityRecord(); - public static final OnUserSlashAction USER_SLASH_ACTION = new OnUserSlashAction(); - public static final OnUpdateTimestampOffsetLock UPDATE_TIMESTAMP_OFFSET_LOCK = new OnUpdateTimestampOffsetLock(); - public static final OnInlineQueries INLINE_QUERY = new OnInlineQueries(); - public static final OnCallMe CALL_ME = new OnCallMe(); - public static final OnEventHackHandle EVENT_HACK_HANDLE = new OnEventHackHandle(); - @SuppressWarnings("unused") static final OnKuohuanhuanNeedSleep KUOHUANHUAN_NEED_SLEEP = new OnKuohuanhuanNeedSleep(); - public static final OnUserRandoms USER_RANDOMS = new OnUserRandoms(); - public static final OnCallMsgSend CALL_MSG_SEND = new OnCallMsgSend(); - public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply(); - public static final OnRandomlyTriggered RANDOMLY_TRIGGERED = new OnRandomlyTriggered(); - - public static void registerAllListeners () { - EventListenerManager.addListener( -// ACTIVITY_RECORDER, - UPDATE_TIMESTAMP_OFFSET_LOCK, - /* write functional event behind here */ -// KUOHUANHUAN_NEED_SLEEP, - COMMANDS_LISTENER, - RANDOMLY_TRIGGERED, - USER_RANDOMS, - USER_SLASH_ACTION, - INLINE_QUERY, - CALL_ME, - CALL_MSG_SEND, - MEDICATION_NOTIFY_APPLY, - EVENT_HACK_HANDLE - ); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java deleted file mode 100644 index 6b26436..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java +++ /dev/null @@ -1,27 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.daemon.TrackerDataManager; -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -public class OnActivityRecord extends EventListener { - - @Override - public boolean onMessage (@Nonnull Update update) { - if ( - update.message().chat().type() == Chat.Type.supergroup || - update.message().chat().type() == Chat.Type.group - ) { - TrackerDataManager.record( - update.message().chat().id(), - update.message().from().id(), - (long)update.message().date() * 1000 - ); - } - return super.onMessage(update); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java deleted file mode 100644 index c448881..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java +++ /dev/null @@ -1,182 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.MornyTrusted; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.CommonFormat; -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.ForwardMessage; -import com.pengrad.telegrambot.request.GetChat; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; -import com.pengrad.telegrambot.response.SendResponse; - -import javax.annotation.Nonnull; - -/** - * 通过 bot 呼叫主人的事件监听管理类 - * @since 0.4.2.1 - */ -public class OnCallMe extends EventListener { - - /** - * 主人的 telegram user id,同时被用于 chat id
- * 跟随 {@link MornyTrusted#MASTER} 的值 - * @since 0.4.2.1 - */ - private static final long ME = MornyCoeur.trustedInstance().MASTER; - - /** - * 监听私聊 bot 的消息进行呼叫关键字匹配。 - * 如果成功,将会执行呼叫函数,并向呼叫者回显{@link TelegramStickers#ID_WAITING "已呼叫"贴纸} - * - * @param update 事件基础参数,消息事件所属的 tgapi:update 对象 - * @return 事件基础返回值,是否已完成处理事件:
- * 如果匹配到呼叫,则返回{@code true},反之返回{@code false} - */ - @Override - public boolean onMessage (@Nonnull Update update) { - if (update.message().text() == null) - return false; - if (update.message().chat().type() != Chat.Type.Private) - return false; - switch (update.message().text().toLowerCase()) { - case "steam", "sbeam", "sdeam" -> - requestSteamJoin(update); - case "hana paresu", "花宫", "内群" -> - requestHanaParesuJoin(update); - case "dinner", "lunch", "breakfast", "meal", "eating", "安妮今天吃什么" -> - requestLastDinner(update); - default -> { - if (update.message().text().startsWith("cc::")) { - requestCustomCall(update); - break; - } - return false; - } - } - MornyCoeur.extra().exec(new SendSticker( - update.message().chat().id(), - TelegramStickers.ID_SENT - ).replyToMessageId(update.message().messageId()) - ); - return true; - } - - /** - * 执行 steam library 呼叫
- * 将会向 {@link #ME} 发送 - * - * @param event 执行呼叫的tg事件 - */ - private static void requestSteamJoin (Update event) { - MornyCoeur.extra().exec(new SendMessage( - ME, String.format( - """ - request STEAM LIBRARY - from %s""", - TGToString.as(event.message().from()).fullnameRefHtml() - ) - ).parseMode(ParseMode.HTML)); - } - - /** - * 执行花宫呼叫
- * 将会向 {@link #ME} 发送 - * - * @param event 执行呼叫的tg事件 - */ - private static void requestHanaParesuJoin (Update event) { - MornyCoeur.extra().exec(new SendMessage( - ME, String.format( - """ - request Hana Paresu - from %s""", - TGToString.as(event.message().from()).fullnameRefHtml() - ) - ).parseMode(ParseMode.HTML)); - } - - /** - * 对访问最近一次的饭局的请求进行回复
- * - * @param event 执行呼叫的tg事件 - */ - private static void requestLastDinner (Update event) { - boolean isAllowed = false; - Message lastDinnerData = null; - if (MornyCoeur.trustedInstance().isTrustedForDinnerRead(event.message().from().id())) { - lastDinnerData = MornyCoeur.extra().exec(new GetChat(MornyCoeur.DINNER_CHAT_ID)).chat().pinnedMessage(); - SendResponse sendResp = MornyCoeur.extra().exec(new ForwardMessage( - event.message().from().id(), - lastDinnerData.forwardFromChat().id(), - lastDinnerData.forwardFromMessageId() - )); - MornyCoeur.extra().exec(new SendMessage( - event.message().from().id(), - String.format("on %s [UTC+8]\n- %s before", - MsgEscape.escapeHtml( - CommonFormat.formatDate((long)lastDinnerData.forwardDate()*1000, 8) - ), MsgEscape.escapeHtml( - CommonFormat.formatDuration(System.currentTimeMillis()-(long)lastDinnerData.forwardDate()*1000) - ) - ) - ).replyToMessageId(sendResp.message().messageId()).parseMode(ParseMode.HTML)); - isAllowed = true; - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().from().id(), - TelegramStickers.ID_403 - ).replyToMessageId(event.message().messageId())); - } - MornyCoeur.extra().exec(new SendMessage( - ME, String.format( - """ - request Last Annie Dinner - from %s - %s""", - TGToString.as(event.message().from()).fullnameRefHtml(), - isAllowed ? "Allowed and returned " + String.format( - "https://t.me/c/%d/%d", Math.abs(lastDinnerData.forwardFromChat().id()+1000000000000L), lastDinnerData.forwardFromMessageId() - ) : "Forbidden by perm check." - ) - ).parseMode(ParseMode.HTML)); - } - - /** - * 执行自定义呼叫
- * 将会向 {@link #ME} 发送一个 request 数据消息和转发的原始请求消息
- *
- * known issue
    - *
  • 无法处理与转发带有媒体的消息
  • - *
- *
- * 现在你可以通过这个 bot 来呼叫主人(sukazyo)任何事情了 —— - * 但是直接私聊sukazyo不好吗 - * - * @param event 执行呼叫的tg事件 - * @since 0.4.2.2 - */ - private static void requestCustomCall (Update event) { - MornyCoeur.extra().exec(new SendMessage( - ME, String.format( - """ - request [???] - from %s""", - TGToString.as(event.message().from()).fullnameRefHtml() - ) - ).parseMode(ParseMode.HTML)); - MornyCoeur.extra().exec(new ForwardMessage( - ME, - event.message().chat().id(), - event.message().messageId() - )); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java deleted file mode 100644 index d02beae..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java +++ /dev/null @@ -1,221 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.MessageEntity; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.GetChat; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import com.pengrad.telegrambot.response.GetChatResponse; -import com.pengrad.telegrambot.response.SendResponse; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - - -public class OnCallMsgSend extends EventListener { - - private static final Pattern REGEX_MSG_SENDREQ_DATA_HEAD = Pattern.compile("^\\*msg([\\d-]+)(\\*\\S+)?\\n([\\s\\S]+)$"); - - private record MessageToSend ( - String message, - MessageEntity[] entities, - ParseMode parseMode, - long targetId - ) { } - - @Override - public boolean onMessage(Update update) { - - // 执行体检查 - if (update.message().chat().type() != Chat.Type.Private) return false; - if (update.message().text() == null) return false; - if (!update.message().text().startsWith("*msg")) return false; - - // 权限检查 - if (!MornyCoeur.trustedInstance().isTrusted(update.message().from().id())) { - MornyCoeur.extra().exec(new SendSticker( - update.message().chat().id(), - TelegramStickers.ID_403 - ).replyToMessageId(update.message().messageId())); - return true; - } - - Message msgsendReqRaw; // 用户书写的发送请求原文 - MessageToSend msgsendReqBody; // 解析后的发送请求实例 - - // *msgsend 发送标识 - // 处理发送要求 - if (update.message().text().equals("*msgsend")) { - // 发送体处理 - if (update.message().replyToMessage() == null) return answer404(update); - msgsendReqBody = parseRequest(update.message().replyToMessage()); - if (msgsendReqBody == null) return answer404(update); - // 执行发送任务 - SendResponse sendResponse = MornyCoeur.getAccount().execute(parseMessageToSend(msgsendReqBody)); - if (!sendResponse.isOk()) { // 发送失败 - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), - String.format(""" - %d FAILED - %s""", - sendResponse.errorCode(), - sendResponse.description() - ) - ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); - } else { // 发送成功信号 - MornyCoeur.extra().exec(new SendSticker( - update.message().chat().id(), - TelegramStickers.ID_SENT - ).replyToMessageId(update.message().messageId())); - } - return true; - // 发送完成/失败 - 事件结束 - } - - // *msg 检查标识 - if (update.message().text().equals("*msg")) { // 处理对曾经的原文的检查 - if (update.message().replyToMessage() == null) { - return answer404(update); - } - msgsendReqRaw = update.message().replyToMessage(); - } else if (update.message().text().startsWith("*msg")) { // 对接受到的原文进行检查 - msgsendReqRaw = update.message(); - } else { - return answer404(update); // 未定义的动作 - } - - // 对发送请求的用户原文进行解析 - msgsendReqBody = parseRequest(msgsendReqRaw); - if (msgsendReqBody == null) { - return answer404(update); - } - - // 输出发送目标信息 - GetChatResponse targetChatReq = MornyCoeur.getAccount().execute(new GetChat(msgsendReqBody.targetId())); - if (!targetChatReq.isOk()) { - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), - String.format(""" - %d FAILED - %s""", - targetChatReq.errorCode(), - targetChatReq.description() - ) - ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); - } else { - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), - targetChatReq.chat().type() == Chat.Type.Private ? ( - String.format(""" - %d@%s - 🔒 %s %s""", - msgsendReqBody.targetId(), - escapeHtml(targetChatReq.chat().type().name()), - escapeHtml(targetChatReq.chat().firstName()+(targetChatReq.chat().lastName()==null?"":" "+targetChatReq.chat().lastName())), - targetChatReq.chat().username()==null? - String.format("@@", targetChatReq.chat().id()): - (escapeHtml("@"+targetChatReq.chat().username())) - ) - ) : ( - String.format(""" - %d@%s::: - %s %s%s""", - msgsendReqBody.targetId(), - escapeHtml(targetChatReq.chat().type().name()), - switch (targetChatReq.chat().type()) { - case group -> "💭"; - case channel -> "📢"; - case supergroup -> "💬"; - default -> "⭕️"; - }, - escapeHtml(targetChatReq.chat().title()), - targetChatReq.chat().username() != null?String.format( - " @%s", escapeHtml(targetChatReq.chat().username()) - ):"" - ) - ) - ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); - } - // 发送文本测试 - SendResponse testSendResp = MornyCoeur.getAccount().execute( - parseMessageToSend(msgsendReqBody, update.message().chat().id()).replyToMessageId(update.message().messageId()) - ); - if (!testSendResp.isOk()) { - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), - String.format(""" - %d FAILED - %s""", - testSendResp.errorCode(), - testSendResp.description() - ) - ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); - } - - return true; - - } - - @Nullable - private static MessageToSend parseRequest (@Nonnull Message requestBody) { - - final Matcher matcher = REGEX_MSG_SENDREQ_DATA_HEAD.matcher(requestBody.text()); - if (matcher.matches()) { - long targetId = Long.parseLong(matcher.group(1)); - ParseMode parseMode = matcher.group(2) == null ? null : switch (matcher.group(2)) { - case "*markdown", "*md", "*m↓" -> ParseMode.MarkdownV2; - case "*md1" -> ParseMode.Markdown; - case "*html" -> ParseMode.HTML; - default -> null; - }; - final int offset = "*msg".length()+matcher.group(1).length()+(matcher.group(2)==null?0:matcher.group(2).length())+1; - final ArrayList entities = new ArrayList<>(); - if (requestBody.entities() != null) for (MessageEntity entity : requestBody.entities()) { - final MessageEntity parsed = new MessageEntity(entity.type(), entity.offset() - offset, entity.length()); - if (entity.url() != null) parsed.url(entity.url()); - if (entity.user() != null) parsed.user(entity.user()); - if (entity.language() != null) parsed.language(entity.language()); - entities.add(parsed); - } - return new MessageToSend(matcher.group(3), entities.toArray(MessageEntity[]::new), parseMode, targetId); - } - - return null; - - } - - @Nonnull - private static SendMessage parseMessageToSend (@Nonnull MessageToSend body) { - return parseMessageToSend(body, body.targetId); - } - - @Nonnull - private static SendMessage parseMessageToSend (@Nonnull MessageToSend body, long targetId) { - SendMessage sendingBody = new SendMessage(targetId, body.message); - if (body.entities != null) sendingBody.entities(body.entities); - if (body.parseMode != null) sendingBody.parseMode(body.parseMode); - return sendingBody; - } - - private static boolean answer404 (@Nonnull Update update) { - MornyCoeur.extra().exec(new SendSticker( - update.message().chat().id(), - TelegramStickers.ID_404 - ).replyToMessageId(update.message().messageId())); - return true; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java deleted file mode 100644 index ffba9b5..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java +++ /dev/null @@ -1,145 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape; - -import com.google.gson.GsonBuilder; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import java.util.HashMap; -import java.util.Map; - -import static cc.sukazyo.cono.morny.Log.logger; - -/** - * 事件劫持与序列化工具. - * @since 0.4.2.0 - */ -public class OnEventHackHandle extends EventListener { - - /** 事件劫持请求列表 */ - private static final Map hackers = new HashMap<>(); - - /** - * 触发事件劫持的限定条件. - * @since 0.4.2.0 - */ - public enum HackType { - /** 只有相同用户发起的事件才会被触发 */ - USER, - /** 只有相同群组内发生的事件才会触发 */ - GROUP, - /** 任何事件都可以触发 */ - ANY - } - - public record Hacker(long fromChatId, long fromMessageId) { - @Override public String toString() { - return fromChatId + "/" + fromMessageId; - } - } - - /** - * @since 0.4.2.0 - */ - public static void registerHack(long fromMessageId, long fromUserId, long fromChatId, @Nonnull HackType type) { - String rec = null; - switch (type) { - case USER -> rec = String.format("((%d))", fromUserId); - case GROUP -> rec = String.format("{{%d}}", fromChatId); - case ANY -> rec = "[[]]"; - } - hackers.put(rec, new Hacker(fromChatId, fromMessageId)); - logger.debug("add hacker track " + rec); - } - - private boolean onEventHacked (Update update, long chatId, long fromUser) { - logger.debug(String.format("got event signed {{%d}}((%d))", chatId, fromUser)); - Hacker x; - x = hackers.remove(String.format("((%d))", fromUser)); - if (x == null) x = hackers.remove(String.format("{{%d}}", chatId)); - if (x == null) x = hackers.remove("[[]]"); - if (x == null) return false; - logger.debug("hacked event by " + x); - MornyCoeur.extra().exec(new SendMessage(x.fromChatId, String.format( - "%s", - MsgEscape.escapeHtml(new GsonBuilder().setPrettyPrinting().create().toJson(update)) - )).parseMode(ParseMode.HTML).replyToMessageId((int)x.fromMessageId)); - return true; - } - - @Override - public boolean onMessage (@Nonnull Update update) { - return onEventHacked(update, update.message().chat().id(), update.message().from().id()); - } - - @Override - public boolean onEditedMessage (@Nonnull Update update) { - return onEventHacked(update, update.editedMessage().chat().id(), update.editedMessage().from().id()); - } - - @Override - public boolean onChannelPost (@Nonnull Update update) { - return onEventHacked(update, update.channelPost().chat().id(), update.channelPost().chat().id()); - } - - @Override - public boolean onEditedChannelPost (@Nonnull Update update) { - return onEventHacked(update, update.editedChannelPost().chat().id(), update.editedChannelPost().chat().id()); - } - - @Override - public boolean onInlineQuery (@Nonnull Update update) { - return onEventHacked(update, 0, update.inlineQuery().from().id()); - } - - @Override - public boolean onChosenInlineResult (@Nonnull Update update) { - return onEventHacked(update, 0, update.chosenInlineResult().from().id()); - } - - @Override - public boolean onCallbackQuery (@Nonnull Update update) { - return onEventHacked(update, 0, update.callbackQuery().from().id()); - } - - @Override - public boolean onShippingQuery (@Nonnull Update update) { - return onEventHacked(update, 0, update.shippingQuery().from().id()); - } - - @Override - public boolean onPreCheckoutQuery (@Nonnull Update update) { - return onEventHacked(update, 0, update.preCheckoutQuery().from().id()); - } - - @Override - public boolean onPoll (@Nonnull Update update) { - return onEventHacked(update, 0, 0); - } - - @Override - public boolean onPollAnswer (@Nonnull Update update) { - return onEventHacked(update, 0, update.pollAnswer().user().id()); - } - - @Override - public boolean onMyChatMemberUpdated (@Nonnull Update update) { - return onEventHacked(update, update.myChatMember().chat().id(), update.myChatMember().from().id()); - } - - @Override - public boolean onChatMemberUpdated (@Nonnull Update update) { - return onEventHacked(update, update.chatMember().chat().id(), update.chatMember().from().id()); - } - - @Override - public boolean onChatJoinRequest (@Nonnull Update update) { - return onEventHacked(update, update.chatJoinRequest().chat().id(), update.chatJoinRequest().from().id()); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnInlineQueries.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnInlineQueries.java deleted file mode 100644 index ed06f0b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnInlineQueries.java +++ /dev/null @@ -1,47 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResult; -import com.pengrad.telegrambot.request.AnswerInlineQuery; - -import javax.annotation.Nonnull; -import java.util.List; - -/** - * telegram inlineQuery 功能的处理类, - * 也是一个 InlineQueryManager(还没做) - * - * @since 0.4.1.3 - */ -public class OnInlineQueries extends EventListener { - - /** - * @since 0.4.1.3 - */ - @Override - public boolean onInlineQuery (@Nonnull Update update) { - - List> results = MornyCoeur.queryManager().query(update); - - int cacheTime = Integer.MAX_VALUE; - boolean isPersonal = InlineQueryUnit.DEFAULT_INLINE_PERSONAL_RESP; - InlineQueryResult[] inlineQueryResults = new InlineQueryResult[results.size()]; - for (int i = 0; i < results.size(); i++) { - inlineQueryResults[i] = results.get(i).result; - if (cacheTime > results.get(i).cacheTime()) cacheTime = results.get(i).cacheTime(); - if (results.get(i).isPersonal()) isPersonal = true; - } - - if (results.size() == 0) return false; - - MornyCoeur.extra().exec(new AnswerInlineQuery( - update.inlineQuery().id(), inlineQueryResults - ).cacheTime(cacheTime).isPersonal(isPersonal)); - return true; - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java deleted file mode 100644 index 41091aa..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java +++ /dev/null @@ -1,38 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.DeleteMessage; - -import javax.annotation.Nonnull; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.Locale; - -public class OnKuohuanhuanNeedSleep extends EventListener { - - @Override - public boolean onMessage (@Nonnull Update update) { - final GregorianCalendar time = new GregorianCalendar(Locale.TAIWAN); - time.setTimeInMillis(System.currentTimeMillis()); - if ( - ( update.message().from().id() == 786563752L && ( - time.get(Calendar.HOUR_OF_DAY) >= 23 || - time.get(Calendar.HOUR_OF_DAY) < 5 - )) || ( update.message().from().id() == 1075871712L && ( - (time.get(Calendar.HOUR_OF_DAY) >= 22 && time.get(Calendar.MINUTE) >= 30) || - time.get(Calendar.HOUR_OF_DAY) >= 23 || - time.get(Calendar.HOUR_OF_DAY) < 5 - )) - ) { - MornyCoeur.extra().exec( - new DeleteMessage(update.message().chat().id(), - update.message().messageId()) - ); - return true; - } - return false; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java deleted file mode 100644 index a5bd835..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java +++ /dev/null @@ -1,28 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.daemon.MedicationTimer; -import cc.sukazyo.cono.morny.daemon.MornyDaemons; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.Update; -import org.jetbrains.annotations.NotNull; - -public class OnMedicationNotifyApply extends EventListener { - - @Override - public boolean onEditedChannelPost (@NotNull Update update) { - return editedMessageProcess(update.editedChannelPost()); - } - - @Override - public boolean onEditedMessage (@NotNull Update update) { - return editedMessageProcess(update.editedMessage()); - } - - private boolean editedMessageProcess (Message edited) { - if (edited.chat().id() != MedicationTimer.NOTIFY_CHAT) return false; - MornyDaemons.medicationTimerInstance.refreshNotificationWrite(edited); - return true; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java deleted file mode 100644 index d710459..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java +++ /dev/null @@ -1,26 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.bot.api.EventListener; - -public class OnRandomlyTriggered extends EventListener { - - /** - * function CODE_IK0XA1 - */ - // @Override -// public boolean onMessage (@Nonnull Update update) { -// -// if (update.message().text() == null) return false; -// -// if (update.message().text().contains("急") && Math.random()<(1d/20)) { -// MornyCoeur.extra().exec(new SendMessage( -// update.message().chat().id(), -// "急也没用" -// )); -// } -// -// return false; -// -// } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java deleted file mode 100644 index a74e7a5..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; - -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class OnTelegramCommand extends EventListener { - - @Override - public boolean onMessage (@Nonnull Update event) { - if (event.message().text() == null || !event.message().text().startsWith("/") || event.message().text().startsWith("/ ")) { - logger.debug("not command"); - return false; // 检测到非(命令格式)文本,忽略掉命令处理 - } - final InputCommand command = new InputCommand(event.message().text().substring(1)); - if (!command.getCommand().matches("^\\w+$")) { logger.debug("not command");return false; } - logger.debug("is command"); - if (command.getTarget() != null && !MornyCoeur.getUsername().equals(command.getTarget())) { - return true; // 检测到命令并非针对 morny,退出整个事件处理链 - } - return MornyCoeur.commandManager().execute(command, event); // 转交命令管理器执行命令 - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java deleted file mode 100644 index 80eff7f..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java +++ /dev/null @@ -1,56 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -/** - * 阻止 {@link MornyCoeur#latestEventTimestamp 指定时间} 之前的事件处理. - *

- * 只支持以下事件 - *

    - *
  • {@link EventListener#onMessage(Update) 收到消息}
  • - *
  • {@link EventListener#onEditedMessage(Update) 消息被更新}
  • - *
  • {@link EventListener#onChannelPost(Update) 收到频道消息}
  • - *
  • {@link EventListener#onEditedChannelPost(Update) 频道消息被更新}
  • - *
- * @see #isOutdated 时间判断 - */ -public class OnUpdateTimestampOffsetLock extends EventListener { - - /** - * 检查传入时间是否在要求时间之前(即"过期"). - * @param timestamp 传入时间,秒级 - * @return 如果传入时间在要求时间之前,返回true,反之false - * @since 0.4.2.7 - */ - public boolean isOutdated(long timestamp) { - return timestamp < MornyCoeur.getLatestEventTimestamp()/1000; - } - - @Override - public boolean onMessage (@Nonnull Update update) { - return isOutdated(update.message().date()); - } - - /** @since 0.4.2.6 */ - @Override - public boolean onEditedMessage (@Nonnull Update update) { - return isOutdated(update.editedMessage().editDate()); - } - - /** @since 0.4.2.6 */ - @Override - public boolean onChannelPost (@Nonnull Update update) { - return isOutdated(update.channelPost().date()); - } - - /** @since 0.4.2.6 */ - @Override - public boolean onEditedChannelPost (@Nonnull Update update) { - return isOutdated(update.editedChannelPost().editDate()); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java deleted file mode 100644 index 898bdac..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java +++ /dev/null @@ -1,52 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.util.UniversalCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.SendMessage; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.ThreadLocalRandom; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class OnUserRandoms extends EventListener { - - private static final Pattern USER_OR_CN_QUERY = Pattern.compile("(.+)还是(.+)"); - private static final Pattern USER_OR_EN_QUERY = Pattern.compile("(.+)or(.+)"); - - @Override - public boolean onMessage (@NotNull Update update) { - - if (update.message().text() == null) return false; - if (!update.message().text().startsWith("/")) return false; - - final String[] preProcess = UniversalCommand.format(update.message().text()); - if (preProcess.length > 1) return false; - final String query = preProcess[0]; - - // ----- START CODE BLOCK COMMENT ----- - // 这里实现思路和代码优化有至少一半是 copilot 和 IDEA 提供的 - // 实现思路都可以从人类手里抢一半贡献太恐怖了aba - String result = null; - final Matcher matcher; - if (query.contains("还是")) { - matcher = USER_OR_CN_QUERY.matcher(query); - } else { - matcher = USER_OR_EN_QUERY.matcher(query); - } - if (matcher.find()) { - result = ThreadLocalRandom.current().nextBoolean() ? matcher.group(1) : matcher.group(2); - } - // ----- STOP CODE BLOCK COMMENT ----- - - if (result == null) return false; - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), result - ).replyToMessageId(update.message().messageId())); - return true; - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.java deleted file mode 100644 index cf1910b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.java +++ /dev/null @@ -1,84 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.util.UniversalCommand; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; - -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; - -import static cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class OnUserSlashAction extends EventListener { - - @Override - public boolean onMessage (@Nonnull Update event) { - final String text = event.message().text(); - if (text == null) return false; - - if (text.startsWith("/")) - { - - /// Due to @Lapis_Apple, we stopped slash action function at .DP7 groups. - /// It may be enabled after some updates when the function will not be conflicted to other bots. - // if (event.message().chat().id() == ) return false; -//{ if (event.message().chat().title() != null && event.message().chat().title().contains(".DP7")) { -// logger.info(String.format(""" -// Chat slash action ignored due to the following keyword. -// - %s -// - ".DP7\"""", -// TGToString.as(event.message().chat()).toStringFullNameId() -// )); -// return false; -// } - - final String[] action = UniversalCommand.format(text); - action[0] = action[0].substring(1); - - if (action[0].matches("^\\w+(@\\w+)?$")) { - return false; // 忽略掉 Telegram 命令格式的输入 - } else if (action[0].contains("/")) { - return false; // 忽略掉疑似目录格式的输入 - } - - final boolean isHardParse = "".equals(action[0]); - /* 忽略空数据 */ if (isHardParse && action.length < 2) { return false; } - final String verb = isHardParse ? action[1] : action[0]; - final boolean hasObject = action.length != (isHardParse?2:1); - final String object = - hasObject ? - stringsConnecting(action, " ", isHardParse?2:1, action.length-1) : - ""; - final Message origin = event.message(); - final Message target = (event.message().replyToMessage() == null ? ( - origin - ): ( - event.message().replyToMessage() - )); - - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format( - "%s %s%s %s %s!", - TGToString.as(origin).getSenderFirstNameRefHtml(), - escapeHtml(verb), escapeHtml((hasObject?"":"了")), - origin==target ? - "自己" : - TGToString.as(target).getSenderFirstNameRefHtml(), - escapeHtml(hasObject ? object+" " : "") - ) - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - - return true; - - } - return false; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.java deleted file mode 100644 index c79ab37..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.java +++ /dev/null @@ -1,15 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import javax.annotation.Nullable; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; - -import java.util.List; - -public interface ITelegramQuery { - - @Nullable - List> query (Update event); - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/MornyQueries.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/MornyQueries.java deleted file mode 100644 index b0dc135..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/MornyQueries.java +++ /dev/null @@ -1,31 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.List; - -public class MornyQueries { - - private final List queryInstances = new ArrayList<>(); - - public MornyQueries () { - queryInstances.add(new RawText()); - queryInstances.add(new MyInformation()); - queryInstances.add(new ShareToolTwitter()); - queryInstances.add(new ShareToolBilibili()); - } - - @Nonnull - public List> query (@Nonnull Update event) { - final List> results = new ArrayList<>(); - for (ITelegramQuery instance : queryInstances) { - final List> r = instance.query(event); - if (r!=null) results.addAll(r); - } - return results; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/MyInformation.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/MyInformation.java deleted file mode 100644 index 7921174..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/MyInformation.java +++ /dev/null @@ -1,35 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import javax.annotation.Nullable; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResultArticle; -import com.pengrad.telegrambot.model.request.InputTextMessageContent; -import com.pengrad.telegrambot.model.request.ParseMode; - -import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation; - -import java.util.Collections; -import java.util.List; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; - -public class MyInformation implements ITelegramQuery { - - public static final String ID_PREFIX = "[morny/info/me]"; - public static final String TITLE = "My Account Information"; - - @Override - @Nullable - public List> query(Update event) { - if (!(event.inlineQuery().query() == null || "".equals(event.inlineQuery().query()))) return null; - return Collections.singletonList(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX), TITLE, - new InputTextMessageContent( - TelegramUserInformation.informationOutputHTML(event.inlineQuery().from()) - ).parseMode(ParseMode.HTML) - )).isPersonal(true).cacheTime(10)); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/RawText.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/RawText.java deleted file mode 100644 index a773f6b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/RawText.java +++ /dev/null @@ -1,31 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; - -import javax.annotation.Nullable; - -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResultArticle; -import com.pengrad.telegrambot.model.request.InputTextMessageContent; - -import java.util.Collections; -import java.util.List; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; - -public class RawText implements ITelegramQuery { - - public static final String ID_PREFIX = "[morny/r/text]"; - public static final String TITLE = "Raw Text"; - - @Override - @Nullable - public List> query (Update event) { - if (event.inlineQuery().query() == null || "".equals(event.inlineQuery().query())) return null; - return Collections.singletonList(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX, event.inlineQuery().query()), TITLE, - new InputTextMessageContent(event.inlineQuery().query()) - ))); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java deleted file mode 100644 index a5e1ced..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java +++ /dev/null @@ -1,80 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import cc.sukazyo.cono.morny.util.BiliTool; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResultArticle; -import com.pengrad.telegrambot.model.request.InputTextMessageContent; -import com.pengrad.telegrambot.model.request.ParseMode; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -//import static cc.sukazyo.cono.morny.Log.logger; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; - -public class ShareToolBilibili implements ITelegramQuery { - - public static final String TITLE_BILI_AV = "[bilibili] Share video / av"; - public static final String TITLE_BILI_BV = "[bilibili] Share video / BV"; - public static final String ID_PREFIX_BILI_AV = "[morny/share/bili/av]"; - public static final String ID_PREFIX_BILI_BV = "[morny/share/bili/bv]"; - public static final Pattern REGEX_BILI_VIDEO = Pattern.compile("^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?:av|AV)(\\d+)|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]+))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d+)|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]+))$"); - - private static final String SHARE_FORMAT_HTML = "%s"; - - @Nullable - @Override - public List> query (Update event) { - if (event.inlineQuery().query() == null) return null; - final Matcher regex = REGEX_BILI_VIDEO.matcher(event.inlineQuery().query()); - if (regex.matches()) { - -// logger.debug(String.format( -// "====== ok\n1: %s\n2: %s\n3: %s\n4: %s\n5: %s\n6: %s\n7: %s", -// regex.group(1), regex.group(2), regex.group(3), regex.group(4), -// regex.group(5), regex.group(6), regex.group(7) -// )); - - // get video id from input, also get video part id - String av = regex.group(2)==null ? regex.group(6)==null ? null : regex.group(6) : regex.group(2); - String bv = regex.group(3)==null ? regex.group(7)==null ? null : regex.group(7) : regex.group(3); -// logger.trace(String.format("catch id av[%s] bv[%s]", av, bv)); - final int part = regex.group(5)==null ? -1 : Integer.parseInt(regex.group(5)); -// logger.trace(String.format("catch part [%s]", part)); - if (av == null) { - assert bv != null; - av = String.valueOf(BiliTool.toAv(bv)); -// logger.trace(String.format("converted bv[%s] to av[%s]", bv, av)); - } else { - bv = BiliTool.toBv(Long.parseLong(av)); -// logger.trace(String.format("converted av[%s] to bv[%s]", av, bv)); - } - // build standard share links - final String linkPartParam = part==-1 ? "" : "?p="+part; - final String linkAv = "https://www.bilibili.com/video/av"+av + linkPartParam; - final String linkBv = "https://www.bilibili.com/video/BV"+bv + linkPartParam; - final String idAv = "av"+av; - final String idBv = "BV"+bv; -// logger.trace("built all data."); - - // build share message element - List> result = new ArrayList<>(); - result.add(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av, - new InputTextMessageContent(String.format(SHARE_FORMAT_HTML, linkAv, idAv)).parseMode(ParseMode.HTML) - ))); - result.add(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX_BILI_BV+bv), TITLE_BILI_BV+bv, - new InputTextMessageContent(String.format(SHARE_FORMAT_HTML, linkBv, idBv)).parseMode(ParseMode.HTML) - ))); - return result; - - } - return null; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.java deleted file mode 100644 index 9c50d6f..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.java +++ /dev/null @@ -1,50 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResultArticle; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; - -public class ShareToolTwitter implements ITelegramQuery { - - public static final String TITLE_VX = "[tweet] Share as VxTwitter"; - public static final String TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)"; - public static final String ID_PREFIX_VX = "[morny/share/twitter/vxtwi]"; - public static final String ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]"; - - public static final Pattern REGEX_TWEET_LINK = Pattern.compile( - "^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$"); - - @Nullable - @Override - public List> query (@Nonnull Update event) { - if (event.inlineQuery().query() == null) return null; - final Matcher regex = REGEX_TWEET_LINK.matcher(event.inlineQuery().query()); - if (regex.matches()) { - - List> result = new ArrayList<>(); - - result.add(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX_VX+event.inlineQuery().query()), TITLE_VX, - String.format("https://vxtwitter.com/%s", regex.group(2)) - ))); - result.add(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX_VX_COMBINED+event.inlineQuery().query()), TITLE_VX_COMBINED, - String.format("https://c.vxtwitter.com/%s", regex.group(2)) - ))); - - return result; - - } - return null; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java deleted file mode 100644 index 813640b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java +++ /dev/null @@ -1,78 +0,0 @@ -package cc.sukazyo.cono.morny.daemon; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.CommonFormat; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.MessageEntity; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.EditMessageText; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.response.SendResponse; - -import java.util.ArrayList; -import java.util.List; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class MedicationTimer extends Thread { - - public static final long NOTIFY_CHAT = -1001729016815L; - public static final String NOTIFY_MESSAGE = "\uD83C\uDF65⏲"; - private static final String DAEMON_THREAD_NAME = "TIMER_Medication"; - - private static final long LAST_NOTIFY_ID_NULL = -1L; - private long lastNotify = LAST_NOTIFY_ID_NULL; - - - MedicationTimer () { - super(DAEMON_THREAD_NAME); - } - - @Override - public void run () { - logger.info("MedicationTimer started"); - while (!interrupted()) { - try { - waitToNextRoutine(); - sendNotification(); - } catch (InterruptedException e) { - interrupt(); - logger.info("MedicationTimer was interrupted, will be exit now"); - } catch (Exception e) { - logger.error("Unexpected error occurred"); - e.printStackTrace(System.out); - } - } - logger.info("MedicationTimer stopped"); - } - - private void sendNotification () { - final SendResponse resp = MornyCoeur.extra().exec(new SendMessage(NOTIFY_CHAT, NOTIFY_MESSAGE)); - if (resp.isOk()) lastNotify = resp.message().messageId(); - else lastNotify = LAST_NOTIFY_ID_NULL; - } - - public void refreshNotificationWrite (Message edited) { - if (edited.messageId() != lastNotify) return; - final String editTime = CommonFormat.formatDate(edited.editDate()*1000, 8); - ArrayList entities = new ArrayList<>(); - if (edited.entities() != null) entities.addAll(List.of(edited.entities())); - entities.add(new MessageEntity(MessageEntity.Type.italic, edited.text().length() + "\n-- ".length(), editTime.length())); - EditMessageText sending = new EditMessageText( - NOTIFY_CHAT, - edited.messageId(), - edited.text() + "\n-- " + editTime + " --" - ).parseMode(ParseMode.HTML).entities(entities.toArray(MessageEntity[]::new)); - MornyCoeur.extra().exec(sending); - lastNotify = LAST_NOTIFY_ID_NULL; - } - - private static long calcNextRoutineTimestamp () { - return ((System.currentTimeMillis()+8*60*60*1000) / (12*60*60*1000) + 1) * 12*60*60*1000 - 8*60*60*1000; - } - - private void waitToNextRoutine () throws InterruptedException { - sleep(calcNextRoutineTimestamp() - System.currentTimeMillis()); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java deleted file mode 100644 index d40ba47..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.sukazyo.cono.morny.daemon; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class MornyDaemons { - - public static final MedicationTimer medicationTimerInstance = new MedicationTimer(); - - public static void start () { - logger.info("ALL Morny Daemons starting..."); -// TrackerDataManager.init(); - medicationTimerInstance.start(); - logger.info("Morny Daemons started."); - } - - public static void stop () { - - logger.info("ALL Morny Daemons stopping..."); - -// TrackerDataManager.DAEMON.interrupt(); - medicationTimerInstance.interrupt(); - -// TrackerDataManager.trackingLock.lock(); - try { medicationTimerInstance.join(); } catch (InterruptedException e) { e.printStackTrace(System.out); } - - logger.info("ALL Morny Daemons STOPPED."); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java b/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java deleted file mode 100644 index d0f8ab0..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java +++ /dev/null @@ -1,133 +0,0 @@ -package cc.sukazyo.cono.morny.daemon; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.TreeSet; -import java.util.concurrent.locks.ReentrantLock; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class TrackerDataManager { - - public static final ReentrantLock trackingLock = new ReentrantLock(); - - private static final ReentrantLock recordLock = new ReentrantLock(); - private static HashMap>> record = new HashMap<>(); - - public static final TrackerDaemon DAEMON = new TrackerDaemon(); - public static class TrackerDaemon extends Thread { - - public TrackerDaemon () { this.setName("TRACKER"); } - - @Override - public void run () { - trackingLock.lock(); - logger.info("Tracker started."); - long lastWaitTimestamp = System.currentTimeMillis(); - boolean postProcess = false; - do { - lastWaitTimestamp += 10 * 60 * 1000; - long sleeping = lastWaitTimestamp - System.currentTimeMillis(); - if (sleeping > 0) { - try { Thread.sleep(sleeping); } catch (InterruptedException e) { interrupt(); } - } else { - logger.warn("Tracker may be too busy to process data!!"); - lastWaitTimestamp = System.currentTimeMillis(); - } - if (interrupted()) { - postProcess = true; - logger.info("CALLED TO EXIT! writing cache."); - } - if (record.size() != 0) { - save(); - } - else logger.info("nothing to do yet"); - } while (!postProcess); - trackingLock.unlock(); - logger.info("Tracker exited."); - } - - } - - public static void record (long chat, long user, long timestamp) { - recordLock.lock(); - if (!record.containsKey(chat)) record.put(chat, new HashMap<>()); - HashMap> chatUsers = record.get(chat); - if (!chatUsers.containsKey(user)) chatUsers.put(user, new TreeSet<>()); - TreeSet userRecords = chatUsers.get(user); - userRecords.add(timestamp); - recordLock.unlock(); - } - - public static void init () { - DAEMON.start(); - } - - public static void save () { - logger.info("start writing tracker data."); - save(reset()); - logger.info("done writing tracker data."); - } - - private static HashMap>> reset () { - recordLock.lock(); - HashMap>> recordOld = record; - record = new HashMap<>(); - recordLock.unlock(); - return recordOld; - } - - private static void save (HashMap>> record) { - - { - if (!record.containsKey(0L)) record.put(0L, new HashMap<>()); - HashMap> chatUsers = record.get(0L); - if (!chatUsers.containsKey(0L)) chatUsers.put(0L, new TreeSet<>()); - TreeSet userRecords = chatUsers.get(0L); - userRecords.add(System.currentTimeMillis()); - } - - record.forEach((chat, chatUsers) -> chatUsers.forEach((user, userRecords) -> { - - long dayCurrent = -1; - FileChannel channelCurrent = null; - - for (long timestamp : userRecords) { - try { - - long day = timestamp / (24 * 60 * 60 * 1000); - if (dayCurrent != day) { - if (channelCurrent != null) channelCurrent.close(); - channelCurrent = openFile(chat, user, day); - dayCurrent = day; - } - - assert channelCurrent != null; - channelCurrent.write(ByteBuffer.wrap( - String.format("%d\n", timestamp).getBytes(StandardCharsets.UTF_8) - )); - - } catch (Exception e) { - logger.error(String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp)); - e.printStackTrace(System.out); - } - } - - })); - - } - - private static FileChannel openFile (long chat, long user, long day) throws IOException { - File data = new File(String.format("./data/tracker/%d/%d", chat, user)); - if (!data.isDirectory()) if (!data.mkdirs()) throw new IOException("Cannot create file directory " + data.getPath()); - File file = new File(data, String.valueOf(day)); - if (!file.isFile()) if (!file.createNewFile()) throw new IOException("Cannot create file " + file.getPath()); - return new FileOutputStream(file, true).getChannel(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/data/MornyJrrp.java b/src/main/java/cc/sukazyo/cono/morny/data/MornyJrrp.java deleted file mode 100644 index 029e476..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/data/MornyJrrp.java +++ /dev/null @@ -1,46 +0,0 @@ -package cc.sukazyo.cono.morny.data; - -import cc.sukazyo.cono.morny.util.CommonConvert; -import cc.sukazyo.cono.morny.util.CommonEncrypt; -import com.pengrad.telegrambot.model.User; - -/** - * Morny 的 jrrp 运算类. - * - * @see #getJrrpFromTelegramUser(User,long) - * @see #calcJrrpXmomi(long,long) - * @since 0.4.2.9 - */ -public class MornyJrrp { - - /** - * 通过 telegram 用户和时间戳作为参数获取 jrrp. - * - * @see #calcJrrpXmomi 当前版本的实现算法 {@code Xmomi} - * @since 0.4.2.9 - * @param user telegram 用户 - * @param timestamp 时间戳 - * @return 通过当前版本的算法计算出的用户 jrrp 值,取值为 {@code [0.00, 100.00]} - */ - public static double getJrrpFromTelegramUser (User user, long timestamp) { - return calcJrrpXmomi(user.id(), timestamp / (1000 * 60 * 60 * 24)) * 100.0; - } - - /** - * {@code Xmomi} 版本的 jrrp 算法. - *

- * 算法规则为,将用户id与日期戳链接为 uid@daystamp 这样的字符串, - * 然后通过 MD5 计算出字符串的哈希值,取哈希值前4个字节,将其作为16进制数值表示法转换为取值为 {@code [0x0000, 0xffff]} 的数值, - * 得到的数值除以区间最大值 {@code 0xffff} 即可得到一个分布在 {@code [0.0, 1.0]} 之间的分布值, - * 这个分布值乘以 {@code 100.0},即为计算得到的 jrrp 数值。 - * - * @since 0.4.2.9 - * @param userId telegram 用户 uid - * @param dayStamp unix 时间戳转换为日期单位后的数值. 数值应该在转换前转换时区 - * @return 算法得到的 jrrp 值,取值为 {@code [0.00. 100.00]} - */ - public static double calcJrrpXmomi (long userId, long dayStamp) { - return (double)Long.parseLong(CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(userId + "@" + dayStamp)).substring(0, 4), 16) / (double)0xffff; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java b/src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java deleted file mode 100644 index cd1be74..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java +++ /dev/null @@ -1,50 +0,0 @@ -package cc.sukazyo.cono.morny.data; - -import java.io.IOException; - -import com.google.gson.Gson; - -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; - -public class NbnhhshQuery { - - public static class Word { - public String name; - public String[] trans; - public String[] inputting; - } - - public static class GuessResult { - public Word[] words; - } - - public record GuessReq (String text) { - } - - public static final String API_URL = "https://lab.magiconch.com/api/nbnhhsh/"; - public static final String API_GUESS_METHOD = "guess/"; - public static final String API_GUESS_DATA_TEMPLATE = "{ \"text\": \"%s\" }"; - - private static final OkHttpClient httpClient = new OkHttpClient(); - public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); - - public static GuessResult sendGuess (String text) throws IOException { - final String reqJsonText = new Gson().toJson(new GuessReq(text)); - Request request = new Request.Builder() - .url(API_URL + API_GUESS_METHOD) - .post(RequestBody.create(JSON, reqJsonText)) - .build(); - try (Response response = httpClient.newCall(request).execute()) { - final ResponseBody body = response.body(); - if (body == null) throw new IOException("Null body."); - final String x = "{ \"words\": " + body.string() + " }"; - return new Gson().fromJson(x, GuessResult.class); - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.java b/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.java deleted file mode 100644 index 0f60d81..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.java +++ /dev/null @@ -1,89 +0,0 @@ -package cc.sukazyo.cono.morny.data.ip186; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import javax.annotation.Nonnull; -import java.io.IOException; - -/** - * 通过 {@value #SITE_URL} 进行 {@link #queryIp ip}/{@link #queryWhois whois} 数据查询的工具类 - * - * @since 0.4.2.10 - */ -public class IP186QueryHandler { - - /** - * 请求所使用的 HTTP API 站点链接 - * @since 0.4.2.10 - */ - public static final String SITE_URL = "https://ip.186526.xyz/"; - - /** - * 进行 {@link #queryIp ip 查询}时所使用的 API 参数.
- * 目的使 API 直接返回原始数据 - */ - private static final String QUERY_IP_PARAM = "type=json&format=true"; - - /** - * 进行 {@link #queryWhois whois 查询}时所使用的 API 参数.
- * 目的使 API 直接返回原始数据 - */ - private static final String QUERY_WHOIS_PARAM = "type=plain"; - - /** 请求时使用的 OkHttp 请求工具实例 */ - private static final OkHttpClient httpClient = new OkHttpClient(); - - /** - * 通过 {@value #SITE_URL} 获取 ip 信息. - * @see #QUERY_IP_PARAM 发送请求时所使用的 API 参数 - * @param ip 需要进行查询的 ip - * @return 查询结果。data 根据 {@value #SITE_URL} 的规则以 json 序列化 - * @throws IOException 任何请求或解析错误 - */ - @Nonnull - public static IP186QueryResponse queryIp (String ip) throws IOException { - final String requestUrl = SITE_URL + ip; - return commonQuery(requestUrl, QUERY_IP_PARAM); - } - - /** - * 通过 {@value #SITE_URL} 获取域名信息. - * @see #QUERY_WHOIS_PARAM 发送请求时所使用的 API 参数 - * @param domain 需要进行查询的域名 - * @return 查询结果。data 根据 {@value #SITE_URL} 的规则以 plain 序列化 - * @throws IOException 任何请求或解析错误 - */ - @Nonnull - public static IP186QueryResponse queryWhois (String domain) throws IOException { - final String requestUrl = SITE_URL + "whois/" + domain; - return commonQuery(requestUrl, QUERY_WHOIS_PARAM); - } - - /** - * 将 {@link #queryWhois(String)} 的结果进行裁剪. - *
- * 将会删除返回内容中 {@code >>> XXX <<<} 行以后的注释串, - * 以达到只保留重要信息的目的。 - * - * @see #queryWhois(String) - */ - @Nonnull - public static IP186QueryResponse queryWhoisPretty (String domain) throws IOException { - final IP186QueryResponse raw = queryWhois(domain); - return new IP186QueryResponse(raw.url(), raw.body().substring(0, raw.body().indexOf("<<<")+3)); - } - - @Nonnull - private static IP186QueryResponse commonQuery (String requestUrl, String queryIpParam) throws IOException { - Request request = new Request.Builder().url(requestUrl + "?" + queryIpParam).build(); - try (Response response = httpClient.newCall(request).execute()) { - final ResponseBody body = response.body(); - if (body == null) throw new IOException("Null body."); - return new IP186QueryResponse(requestUrl, body.string()); - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryResponse.java b/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryResponse.java deleted file mode 100644 index f30fb3d..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package cc.sukazyo.cono.morny.data.ip186; - -/** - * {@link IP186QueryHandler} 的请求结果数据的通用封装类. - * - * @since 0.4.2.10 - * @param url 请求数据的人类可读的来源链接,并非api链接 - * @param body API 传回的数据内容 - */ -public record IP186QueryResponse(String url, String body) { -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java b/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java deleted file mode 100644 index ef36993..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java +++ /dev/null @@ -1,73 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnegative; -import javax.annotation.Nonnull; -import java.util.HashMap; -import java.util.Map; - -public class BiliTool { - - private static final long V_CONV_XOR = 177451812L; - private static final long V_CONV_ADD = 8728348608L; - private static final char[] BV_TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF".toCharArray(); - private static final int TABLE_INT = BV_TABLE.length; - private static final Map BV_TABLE_REVERSED = new HashMap<>(); - static { for (int i = 0; i < BV_TABLE.length; i++) BV_TABLE_REVERSED.put(BV_TABLE[i], i); } - private static final char[] BV_TEMPLATE = "1 4 1 7 ".toCharArray(); - private static final int[] BV_TEMPLATE_FILTER = new int[]{9, 8, 1, 6, 2, 4}; - - /** - * Convert a Bilibili AV video id format to BV id format. - *

- * the AV id is a number; the BV id is a special base58 number, it shows as String in programming.
- * eg:
- * while the link {@code https://www.bilibili.com/video/BV17x411w7KC/} - * shows the same with {@code https://www.bilibili.com/video/av170001/}, - * the AV id is {@code 170001}, the BV id is {@code 17x411w7KC} - *

- * for now , the BV id has 10 digits. - * the method available while the av-id < 2^27, while it theoretically available when the av-id < 2^30. - * - * @see mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」? - * - * @param bv the BV id, a string in (a special) base58 number format, without "BV" prefix. - * @return the AV id corresponding to this bv id in Bilibili, formatted as a number. - */ - @Nonnegative - public static long toAv (@Nonnull String bv) { - long av = 0; - for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) { - av += BV_TABLE_REVERSED.get(bv.charAt(BV_TEMPLATE_FILTER[i])) * Math.pow(TABLE_INT,i); - } - return (av-V_CONV_ADD)^V_CONV_XOR; - } - - /** - * Convert a Bilibili BV video id format to AV id format. - *

- * the AV id is a number; the BV id is a special base58 number, it shows as String in programming.
- * eg:
- * while the link {@code https://www.bilibili.com/video/BV17x411w7KC/} - * shows the same with {@code https://www.bilibili.com/video/av170001/}, - * the AV id is {@code 170001}, the BV id is {@code 17x411w7KC} - *

- * for now , the BV id has 10 digits. - * the method available while the av-id < 2^27, while it theoretically available when the av-id < 2^30. - * - * @see mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」? - * - * @param av the (base10) AV id. - * @return the AV id corresponding to this bv id in Bilibili, - * as a (special) base 58 number format without "BV" prefix. - */ - @Nonnull - public static String toBv (@Nonnegative long av) { - av = (av^V_CONV_XOR)+V_CONV_ADD; - final char[] bv = BV_TEMPLATE.clone(); - for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) { - bv[BV_TEMPLATE_FILTER[i]] = BV_TABLE[(int)(Math.floor(av/(Math.pow(TABLE_INT, i)))%TABLE_INT)]; - } - return String.copyValueOf(bv); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java deleted file mode 100644 index 907cdd5..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java +++ /dev/null @@ -1,61 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnegative; -import javax.annotation.Nonnull; - -/** - * 进行简单类型转换等工作的类. - */ -public class CommonConvert { - - /** - * 将字节数组转换成 hex 字符串. - * @param b 字节数组 - * @return String 格式的字节数组的 hex 值(每个字节当中没有分隔符) - * @see #byteToHex(byte) - */ - @Nonnull - public static String byteArrayToHex(@Nonnull byte[] b){ - StringBuilder sb = new StringBuilder(); - for (byte value : b) { - sb.append(byteToHex(value)); - } - return sb.toString(); - } - - /** - * 将一个字节转换成十六进制 hex 字符串. - * @param b 字节值 - * @return String 格式的字节的 hex 值(小写) - */ - @Nonnull - public static String byteToHex(byte b) { - final String hex = Integer.toHexString(b & 0xff); - return hex.length()<2?"0"+hex:hex; - } - - /** - * 将一个字符串数组按照一定规则连接. - *

- * 连接的方式类似于"数据1+分隔符+数据2+分隔符+...+数据n-1+分隔符+数据n" - * - * @param array 需要进行连接的字符串数组,数组中每一个元素会是一个数据 - * @param connector 在每两个传入数据中插入的分隔符 - * @param startIndex 从传入的数据组中的哪一个位置开始(第一个元素的位置是 {@code 0}) - * @param stopIndex 从传入的数据组中的哪一个位置停止(元素位置计算方式同上) - * @return 连接好的字符串 - */ - @Nonnull - public static String stringsConnecting ( - @Nonnull String[] array, @Nonnull String connector, @Nonnegative int startIndex, @Nonnegative int stopIndex - ) { - final StringBuilder builder = new StringBuilder(); - for (int i = startIndex; i < stopIndex; i++) { - builder.append(array[i]); - builder.append(connector); - } - builder.append(array[stopIndex]); - return builder.toString(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java deleted file mode 100644 index 0e9c35d..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java +++ /dev/null @@ -1,144 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnull; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; - -/** - * 用于数据加密或编解码的工具类. - *

- * 出于 java std 中 Base64 的 {@link Base64.Encoder encode}/{@link Base64.Decoder decode} 十分好用,在此不再进行包装。 - */ -public class CommonEncrypt { - - /** - * 在使用加密算法处理字符串时默认会使用的字符串编码. - *

- * Morny 使用 UTF-8 编码因为这是一般而言加解密工具的默认行为 - */ - public static final Charset ENCRYPT_STANDARD_CHARSET = StandardCharsets.UTF_8; - - @Nonnull - private static byte[] hashAsJavaMessageDigest(String algorithm, @Nonnull byte[] data) { - try { - return MessageDigest.getInstance(algorithm).digest(data); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } - - /** - * 取得数据的 md5 散列值. - * - * @param data byte 数组形式的数据体 - * @return 二进制(byte数组)格式的数据的 md5 散列值 - */ - @Nonnull - public static byte[] hashMd5 (@Nonnull byte[] data) { - return hashAsJavaMessageDigest("md5", data); - } - - /** - * 取得一个字符串的 md5 散列值. - *

- * 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析 - * - * @param originString 要进行散列的字符串 - * @return 二进制(byte数组)格式的 md5 散列值 - */ - @Nonnull - public static byte[] hashMd5 (String originString) { - return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET)); - } - - /** - * 取得数据的 sha1 散列值. - * - * @param data byte 数组形式的数据体 - * @return 二进制(byte数组)格式的数据的 sha1 散列值 - */ - @Nonnull - public static byte[] hashSha1 (@Nonnull byte[] data) { - return hashAsJavaMessageDigest("sha1", data); - } - - /** - * 取得一个字符串的 sha1 散列值. - *

- * 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析 - * - * @param originString 要进行散列的字符串 - * @return 二进制(byte数组)格式的 sha1 散列值 - */ - @Nonnull - public static byte[] hashSha1 (String originString) { - return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET)); - } - - /** - * 取得数据的 sha256 散列值. - * - * @param data byte 数组形式的数据体 - * @return 二进制(byte数组)格式的数据的 sha256 散列值 - */ - @Nonnull - public static byte[] hashSha256 (@Nonnull byte[] data) { - return hashAsJavaMessageDigest("sha256", data); - } - - /** - * 取得一个字符串的 sha256 散列值. - *

- * 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析 - * - * @param originString 要进行散列的字符串 - * @return 二进制(byte数组)格式的 sha256 散列值 - */ - @Nonnull - public static byte[] hashSha256 (String originString) { - return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET)); - } - - /** - * 取得数据的 sha512 散列值. - * - * @param data byte 数组形式的数据体 - * @return 二进制(byte数组)格式的数据的 sha512 散列值 - */ - @Nonnull - public static byte[] hashSha512 (@Nonnull byte[] data) { - return hashAsJavaMessageDigest("md5", data); - } - - /** - * 取得一个字符串的 sha512 散列值. - *

- * 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析 - * - * @param originString 要进行散列的字符串 - * @return 二进制(byte数组)格式的 sha512 散列值 - */ - @Nonnull - public static byte[] hashSha512 (String originString) { - return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET)); - } - - @Nonnull - public static String base64FilenameLint (String inputName) { - if (inputName.endsWith(".b64")) { - return inputName.substring(0, inputName.length()-".b64".length()); - } else if (inputName.endsWith(".b64.txt")) { - return inputName.substring(0, inputName.length()-".b64.txt".length()); - } else if (inputName.endsWith(".base64")) { - return inputName.substring(0, inputName.length()-".base64".length()); - } else if (inputName.endsWith(".base64.txt")) { - return inputName.substring(0, inputName.length()-".base64.txt".length()); - } else { - return inputName; - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java deleted file mode 100644 index 0598ba6..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; - -public class CommonFormat { - - public static final String DATE_TIME_PATTERN_FULL_MILLIS = "yyyy-MM-dd HH:mm:ss:SSS"; - - public static String formatDate (long timestamp, int utcOffset) { - return DateTimeFormatter.ofPattern(DATE_TIME_PATTERN_FULL_MILLIS).format(LocalDateTime.ofInstant( - Instant.ofEpochMilli(timestamp), - ZoneId.ofOffset("UTC", ZoneOffset.ofHours(utcOffset)) - )); - } - - public static String formatDuration (long duration) { - StringBuilder sb = new StringBuilder(); - if (duration > 1000 * 60 * 60 * 24) sb.append(duration / (1000*60*60*24)).append("d "); - if (duration > 1000 * 60 * 60) sb.append(duration / (1000*60*60) % 24).append("h "); - if (duration > 1000 * 60) sb.append(duration / (1000*60) % 60).append("min "); - if (duration > 1000) sb.append(duration / 1000 % 60).append("s "); - sb.append(duration % 1000).append("ms"); - return sb.toString(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java b/src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java deleted file mode 100644 index 57ff657..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java +++ /dev/null @@ -1,28 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnull; -import java.io.FileInputStream; -import java.io.IOException; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class FileUtils { - - @Nonnull - public static String getMD5Three (@Nonnull String path) throws IOException, NoSuchAlgorithmException { - final BigInteger bi; - final byte[] buffer = new byte[8192]; - int len; - final MessageDigest md = MessageDigest.getInstance("MD5"); - final FileInputStream fis = new FileInputStream(path); - while ((len = fis.read(buffer)) != -1) { - md.update(buffer, 0, len); - } - fis.close(); - final byte[] b = md.digest(); - bi = new BigInteger(1, b); - return bi.toString(16); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java b/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java deleted file mode 100644 index f9e11b8..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java +++ /dev/null @@ -1,47 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnull; -import java.util.ArrayList; - -public class UniversalCommand { - - @Nonnull - public static String[] format (@Nonnull String com) { - - final ArrayList arr = new ArrayList<>(); - - final StringBuilder tmp = new StringBuilder(); - final char[] coma = com.toCharArray(); - for (int i = 0; i < coma.length; i++) { - if (coma[i] == ' ') { - if (!tmp.toString().equals("")) { arr.add(tmp.toString()); } - tmp.setLength(0); - } else if (coma[i] == '"') { - while (true) { - i++; - if (coma[i] == '"') { - break; - } else if (coma[i] == '\\' && (coma[i+1] == '"' || coma[i+1] == '\\')) { - i++; - tmp.append(coma[i]); - } else { - tmp.append(coma[i]); - } - } - } else if (coma[i] == '\\' && (coma[i+1] == ' ' || coma[i+1] == '"' || coma[i+1] == '\\')) { - i++; - tmp.append(coma[i]); - } else { - tmp.append(coma[i]); - } - } - if (!tmp.toString().equals("")) { arr.add(tmp.toString()); } - tmp.setLength(0); - - final String[] out = new String[arr.size()]; - arr.toArray(out); - return out; - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java deleted file mode 100644 index 784e6c6..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java +++ /dev/null @@ -1,88 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi; - -import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; -import com.pengrad.telegrambot.TelegramBot; -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.ChatMember; -import com.pengrad.telegrambot.model.User; -import com.pengrad.telegrambot.request.BaseRequest; -import com.pengrad.telegrambot.request.GetChatMember; -import com.pengrad.telegrambot.response.BaseResponse; - -public class ExtraAction { - - private final TelegramBot bot; - - public ExtraAction (TelegramBot bot) { - this.bot = bot; - } - - public static ExtraAction as (TelegramBot bot) { - return new ExtraAction(bot); - } - - public boolean isUserInGroup (User user, Chat chat) { - return isUserInGroup(user.id(), chat.id()); - } - - public , R extends BaseResponse> R exec (T req) { - return exec(req, ""); - } - - public , R extends BaseResponse> R exec (T req, String errorMessage) { - final R resp = bot.execute(req); - if (!resp.isOk()) throw new EventRuntimeException.ActionFailed( - (errorMessage.equals("") ? String.valueOf(resp.errorCode()) : errorMessage), - resp - ); - return resp; - } - - public boolean isUserInGroup (User user, Chat chat, ChatMember.Status permissionLevel) { - return isUserInGroup(user.id(), chat.id(), permissionLevel); - } - - public boolean isUserInGroup (long userId, long chatId) { - return isUserInGroup(userId, chatId, ChatMember.Status.restricted); - } - - public boolean isUserInGroup (long userId, long chatId, ChatMember.Status permissionLevel) { - final ChatMember chatMember = exec(new GetChatMember(chatId, userId)).chatMember(); - return - chatMember != null && - UserPermissionLevel.as(chatMember.status()).hasPermission(UserPermissionLevel.as(permissionLevel)); - } - -} - -enum UserPermissionLevel { - - CREATOR(3), - ADMINISTRATOR(2), - MEMBER(1), - RESTRICTED(0), - LEFT(-1), - KICKED(-2); - - final int permissionLevel; - - UserPermissionLevel (int permissionLevel) { - this.permissionLevel = permissionLevel; - } - - static UserPermissionLevel as (ChatMember.Status status) { - return switch (status) { - case creator -> CREATOR; - case administrator -> ADMINISTRATOR; - case member -> MEMBER; - case restricted -> RESTRICTED; - case left -> LEFT; - case kicked -> KICKED; - }; - } - - boolean hasPermission (UserPermissionLevel required) { - return this.permissionLevel >= required.permissionLevel; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java deleted file mode 100644 index a62a3c9..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java +++ /dev/null @@ -1,66 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi; - -import cc.sukazyo.cono.morny.util.UniversalCommand; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Arrays; - -public class InputCommand { - - - private final String target; - private final String command; - private final String[] args; - - private InputCommand (@Nullable String target, @Nonnull String command, @Nonnull String[] args) { - this.target = target; - this.command = command; - this.args = args; - } - - public InputCommand (@Nonnull String[] inputArray) { - this(parseInputArray(inputArray)); - } - - public InputCommand (@Nonnull String input) { - this(UniversalCommand.format(input)); - } - - public InputCommand (@Nonnull InputCommand source) { - this(source.target, source.command, source.args); - } - - public static InputCommand parseInputArray (@Nonnull String[] inputArray) { - final String[] cx = inputArray[0].split("@", 2); - final String[] args = new String[inputArray.length-1]; - System.arraycopy(inputArray, 1, args, 0, inputArray.length - 1); - return new InputCommand(cx.length == 1 ? null : cx[1], cx[0], args); - } - - @Nullable - public String getTarget () { - return target; - } - - @Nonnull - public String getCommand () { - return command; - } - - @Nonnull - public String[] getArgs () { - return args; - } - - public boolean hasArgs () { - return args.length != 0; - } - - @Override - @Nonnull - public String toString() { - return String.format("{{%s}@{%s}#{%s}}", command, target, Arrays.toString(args)); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java deleted file mode 100644 index 368d3b9..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java +++ /dev/null @@ -1,35 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.event; - -import com.pengrad.telegrambot.response.BaseResponse; - -public class EventRuntimeException extends RuntimeException { - - public EventRuntimeException () { - super(); - } - - public EventRuntimeException (String message) { - super(message); - } - - public static class ActionFailed extends EventRuntimeException { - - private final BaseResponse response; - - public ActionFailed (BaseResponse response) { - super(); - this.response = response; - } - - public ActionFailed (String message, BaseResponse response) { - super(message); - this.response = response; - } - - public BaseResponse getResponse() { - return response; - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java deleted file mode 100644 index 06703a4..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java +++ /dev/null @@ -1,15 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import javax.annotation.Nonnull; - -public class MsgEscape { - - @Nonnull - public static String escapeHtml (@Nonnull String raw) { - raw = raw.replaceAll("&", "&"); - raw = raw.replaceAll("<", "<"); - raw = raw.replaceAll(">", ">"); - return raw; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java deleted file mode 100644 index b046f30..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java +++ /dev/null @@ -1,18 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import cc.sukazyo.cono.morny.util.CommonConvert; -import cc.sukazyo.cono.morny.util.CommonEncrypt; - -import javax.annotation.Nonnull; - -public class NamedUtils { - - public static String inlineIds (@Nonnull String tag) { - return inlineIds(tag, ""); - } - - public static String inlineIds (@Nonnull String tag, @Nonnull String taggedData) { - return CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(tag+taggedData)); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java deleted file mode 100644 index 027f515..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java +++ /dev/null @@ -1,21 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.User; - -public class TGToString { - - public static TGToStringFromChat as (Chat chat) { - return new TGToStringFromChat(chat); - } - - public static TGToStringFromUser as (User user) { - return new TGToStringFromUser(user); - } - - public static TGToStringFromMessage as (Message message) { - return new TGToStringFromMessage(message); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java deleted file mode 100644 index 8671056..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java +++ /dev/null @@ -1,22 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.Chat; - -public class TGToStringFromChat { - - private final Chat data; - - public TGToStringFromChat(Chat chat) { - this.data = chat; - } - - public String toStringFullNameId() { - if (data.title() == null) { - throw new IllegalArgumentException("Cannot format private chat to group Name+Id format."); - } - return (data.username() == null) ? - (String.format("%s [%d]", data.title(), data.id())) : - (String.format("%s {%s}[%d]", data.title(), data.username(), data.id())); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java deleted file mode 100644 index 08b2d94..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java +++ /dev/null @@ -1,27 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.Message; - -import javax.annotation.Nonnull; - -public class TGToStringFromMessage extends TGToString { - - @Nonnull - private final Message message; - - public TGToStringFromMessage (@Nonnull Message message) { this.message = message; } - - @Nonnull - public String getSenderFirstNameRefHtml () { - return message.senderChat()==null ? TGToString.as(message.from()).firstnameRefHtml() : String.format( - "%s", - message.senderChat().id(), - MsgEscape.escapeHtml(message.senderChat().title()) - ); - } - - public long getSenderId () { - return message.senderChat()==null ? message.from().id() : message.senderChat().id(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java deleted file mode 100644 index fa41d3c..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java +++ /dev/null @@ -1,53 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.User; - -public class TGToStringFromUser { - - private final User data; - - public TGToStringFromUser (User user) { - this.data = user; - } - - public String fullname () { - return data.firstName() + (data.lastName()==null ? "" : " "+data.lastName()); - } - - public String fullnameRefHtml () { - return String.format( - "%s", - data.id(), - MsgEscape.escapeHtml(fullname()) - ); - } - - public String fullnameRefMarkdown () { - return String.format( - "[%s](tg://user?id=%d)", - fullname(), - data.id() - ); - } - - public String firstnameRefHtml () { - return String.format( - "%s", - data.id(), - MsgEscape.escapeHtml(data.firstName()) - ); - } - - public String firstnameRefMarkdown () { - return String.format( - "[%s](tg://user?id=%d)", - data.firstName(), - data.id() - ); - } - - public String toStringLogTag () { - return (data.username()==null ? fullname()+" " : "@"+data.username()) + "[" + data.id() + "]"; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java deleted file mode 100644 index e1703e4..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java +++ /dev/null @@ -1,85 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.User; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class TelegramUserInformation { - - public static final String DC_QUERY_SOURCE_SITE = "https://t.me/"; - public static final Pattern DC_QUERY_PROCESSOR_REGEX = Pattern.compile("(cdn[1-9]).tele(sco.pe|gram-cdn.org)"); - - private static final OkHttpClient httpClient = new OkHttpClient(); - - @Nullable - public static String getDataCenterFromUsername (String username) { - final Request request = new Request.Builder().url(DC_QUERY_SOURCE_SITE + username).build(); - try (Response response = httpClient.newCall(request).execute()) { - final ResponseBody body = response.body(); - if (body == null) return "empty upstream response"; - final Matcher matcher = DC_QUERY_PROCESSOR_REGEX.matcher(body.string()); - if (matcher.find()) { - return matcher.group(1); - } - } catch (IOException e) { - return e.getMessage(); - } - return null; - } - - public static String informationOutputHTML (User user) { - - final StringBuilder userInformation = new StringBuilder(); - userInformation.append(String.format( - """ - userid : - - %d""", - user.id() - )); - if (user.username() == null) { - userInformation.append("\nusername : null\ndatacenter : null"); - } else { - userInformation.append(String.format( - """ - - username : - - %s""", - escapeHtml(user.username()) - )); - // 依赖 username 的 datacenter 查询 - final String dataCenter = getDataCenterFromUsername(user.username()); - if (dataCenter == null) { userInformation.append("\ndatacenter : null"); } - else { userInformation.append(String.format("\ndatacenter : %s", escapeHtml(dataCenter))); } - } - userInformation.append(String.format( - """ - - display name : - - %s%s""", - escapeHtml(user.firstName()), - user.lastName()==null ? "" : String.format("\n- %s", escapeHtml(user.lastName())) - )); - if (user.languageCode() != null) { - userInformation.append(String.format( - """ - - language-code : - - %s""", - escapeHtml(user.languageCode()) - )); - } - - return userInformation.toString(); - - } - -} diff --git a/src/main/resources/assets_morny/images/featured-image@0.5x.jpg b/src/main/resources/assets_morny/images/featured-image@0.5x.jpg new file mode 100644 index 0000000..444cf8f Binary files /dev/null and b/src/main/resources/assets_morny/images/featured-image@0.5x.jpg differ diff --git a/src/main/resources/assets_morny/texts/server-hello.txt b/src/main/resources/assets_morny/texts/server-hello.txt new file mode 100644 index 0000000..96136ec --- /dev/null +++ b/src/main/resources/assets_morny/texts/server-hello.txt @@ -0,0 +1,55 @@ +ttt///t/////fucj(\tvnxtf{< .' .. .:i` . . ^!`l|-^i+,!_[:1/|{i?//\//jf\\\///\\\\//\\\//////\\/\\\\\\\\\\\\\\//\\\\/\\\\/\\//\\\///\\\\\\\\\\\\\\\\\\\\fnncvvU0O00QCx!!". .. ` +tt//////////\jzjrucnjt/?{j,,"' . .' .. .":. .;{: ' "`.,1(<."i?)\(-}\\\(((\\/\\\\\\\\\\\\\\\\///\//////\\\\\\\\\\\\\\\\\\\\\\\|\\\\\\///\\///\\\\\\\\|\\\\\\|\\\\\\\\tvXvuXcxn/[Il)({_:.. ."` ., +//////////////////\////|)/([}-_<+[]>.^^""[<'`^` .''""`'.`'`"i! ^!>l:' :<" !!.IiI`+l^^`i>_<`??)1;^{\\\\\{|({({|/\]I)\\()\(]}|\\||\|||\/\\\\\\|||\\\\\\\\//\\\\/\\\\||\\\\\\\\//\\\\\\\\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\////\\\\\\\\\//|{{?{|)[[-; +ttt/tt//////////////////{)(\t(/tt/1~I}{-1\_^])1_+[{|(?"<1~>>+!+[}11)}[(1}]};^1\|~_1}{I:-1(I+)(|))|\\/////////\\////\\\/////\\\\\\\\\\\\\\/\//\\///\//||\////|)(//\\///){\/\(11|///({)//({[1\\\\\\\\\\|\/\\\/\//////////\\\\\\\\\//\\\\\///////\|\\\\//////\\///\ +tttt/////////////\///////\||///////t//|(|)|}|\/(\\(//(l_{{. ... ">+<^'I!: ^<(\\1}1//\\\//////////\\///\/\///\\\\\\//\\//\\\\\\\\\\\\\\///\(/\{ +t////////////////////////////////////////////////////////\/\\///////\\\\\\\\\\\\\\\/\\\\\\||\|\\\\\|\\\\/\\|\\\\\\////((|///}!:,":,^`. .;' ' '^..':. ^!;. .^^ '^^`. '' ...I[{!>:^;_i:'~\ttt/////tt//\\////////////\\\\/\\\\\\/\\\\/\\\\\)}-+[+I??i +ttt////////////////////////////////////////////////////////\\//\//\\/\\\\\\\\\\//\\\\\\||\\\/\//\\\\\\\\\/\|\\\////\1;``^;<>+!">__+I `' .. "'. .;" ;;. .:^ ``,,;'` .;]I ,-_-|\////t////t///////\/\\\\//\\\\\\//\\\///////-II1ttt///tttt/////////\/\/////\\\\////t|+<}?!-]l<{[[1-+] +t//////////////////////////////////////\/////////\////////////////\//////\//tttttttttt//////////////////////////)_)t)|}1f/{<.^,^:~: . .. '''^:-|/> '-/}-_?\/)-{?(//\(\tt////\///\\\\\\\///t1.;); .l~` '" +///////////////////////////////////////////tt/t(|tt//]+{t\{][|////\//////////ttttt///t//t/////////////\//////|//{[|f}!l>~++~<<\//]l~?])tt//\\\\\/\\///\\|?<_}["^!;I^;]:. . +////////////////////////////////tttt/|{[1)]~!!+>!<_(/|[-<"i!l,]tt//ttt/t////ttt//t///ttttttt////////t//t//ttt){+. :?^ '. l_-!+l;;;|!!>~~il!lllllllllll!!lI:`'. .' :I;]_}>,?tf:.+fft)l+1//\~`'I-(//\/t/|/(-1[)/?>>II:' '.`';-'` +/////////////////////////t//()\1_<>il^'''' ,!>;.,.'{tti `~tf(`'-(|fffftttttttttt/tttttttttttt///tttft//(t|]?-+!^ ."`. `. ;!I,. .?{il-\_!~<>>!lII;IllIIIIIllllllllllI;;:,:,' '"^`(f{+{>' .<{t(I!}/||t> ^(//}>;:1\]: "[:"` ^<: . II.'.. +///////////////ttt//tt((-!+}"'^. I, ,?<:' ,:;!>~',!_~{}-1]`^!}_+\ttttttt/tttttttfff/tt\(||]-?+;,:"l" '..'.. ?]l:" -(lI;,~?~!IIIlllI:IIlI;IIIIlllllllIIIIIllII!; . . '^^;~), "~!}\/t//\\/_. '". '_i !i''' +tt//t///ttt///(]<>l>][l"'.`,. ^.^. ii ;; ~>>>. .i~I'^^<}), .;|tfftttttttttttf\]}t-!,,I` .^ '. !: . .",I;. ^,I<)/-l:;llllllI;lIll;;IIIIlllIllIlIIlIllI;><. ' .;}". '.:+](ft\}(t/t{;<\{l^>}!^l\/{>1/t(lI:I!+<<". ':" +t//tttt|?+!I!:' '` .`. ...... `^ "<^.;`^"'`,!".,^^^.,?)!. [f/+>(/tttft\tff|+^,!' '^: >[,++:`' .I^ . _?!:^. ;~{/?//)! __::. ':. '. +t/\}[{]",il'`!-<-]:`'^` .. '' .^+:'. .^'"i:`^. ';`:<_|>'.?/t/!"<)ffftf)]]!'II.,l ^' ''. '";" .' `Il, ;]>]j_;lI;ll;!!llIII>~IIIIII;: ;,~.',.<:`, 'I_|\; .i|/]^ ?(}\/////\i' '' ....'^ +tf1<}i `^. `I` .I?"'. . . ^' .^' .'` .". >}_.I|t{_(tf({~,~(); ')t};.><,. .. . .. . . .]}^{j1IlllIlI!1IlllII?{IIIIIlI;[1!I;IIllIIlIIllI<]_;+/t\(|\/1,' "` ... +)+::((:^' ll .,` . . ..'. ' :+'`{tj{,l: ^;"..;!"^.I?' '~; .` .'. . `1+ [x?-:lIlll!]r-IllI~~{~I>lllll[i\--+;;I~IIIII!l;x] "I"-<<_> >i.' l{}:itf/}[/\)(\}))|(:^^..'. `". +>.._f|i.:l,;^^''__. .^' `' "+,`]1i`!1_. ^l: .". .` '1I +JIt!IIlll;]\) >1}c(_i(!IllI_l;(f. ,_";~~+^ .. .;-i '+([i+: !//1](||/\(?:^..^^ +i'"}_,.` ''^... '. `. ^<`_> .. +x??_~]:[|!,.ll` . {+ ;Y[^|,>~IlIIf\ {/;I!\ [[-'<+l-{ _??]f\n]lllI[!;1v` `+"]-}]~" ..'`l, ''i-` l+?\\\/t{!)t[:' .^^ +;,:: :,^..;:. . i+..;^ `_ ]]<-?l``-]' I>]?+. ^-|\\_I?]{t/?` .... +^(\]I^~?;."!" . .. . ^<^ :( >t) _[il>|+:(U<1nYQ0Xx\> . .~xcXXYzx(n?IllI}">xCI .:1]_-" . .^. `}>!}((1-^,+?" .. +1+,~I.(l' . ... ~|r:;`.+I?\};+t) "".-?;lI>(;]xn. '_>]!+. '^'`l:11l[|((+?: . +[-`.':;..""' lv|. .:_(;I!u> ^,",^. .;?I]?IlI}n[^ ')(I' `tlII>x1" <}1{)l "~+ |[II;\[:~zl .i;. .... `"i\\}]..!' ^.... +-" ` ' . ":` .|+<<;!\U)>^ '^`' ^"I?)c-;j/ <1I~;` .!}\(: .;"`' +' 'l, .' ~[><+;!f()nn|]!:' ..^:!+1fcjx}}v!_)})>|n` ~^ ^;"'` .<+I<)/||\i'"<"'^ `. + II ^}_'+_!fI?_/-jJjUr\\ucJJz\|J>}?-j{]^ni" .;](),.;-<`' .^ .^' + +[" +]{.`i;I; ::."!??l.^Ywj}<, (n, ,~_:` .,, ` `` ' '... ^+-l,]}]}\j/!. . ` 'I<~`'{tl..^` + '' .,<{[>" i/i" `-[; ,<_[>^i_l,:^_! ',+l.. ^,:,,. ;~>l;^ l> ',;I^???~,'l".. .. + ;{?l. !+ .. .<1i '^' "}|{:-+-;?\[)-] ^:l1-:. '' '`, . ';.`~^ '. ..^`. + 'i+;]}!,. <))\!<|ji >((_}}?t)}\\v|]?jI!), "lf!l. ... .^ . ". + .+{>` l/z\!,>""I+~_){]vQjut_~~>>>_-<]<-)f":l_v){\/1}}}{t/\0?z~. ^' `-l . . .. . + l\ :_>>i:^+\)_-]!:>-+l'...`^;1!^ 'l>})l\n\Qt?]?]})1{][[(XC>^ ... + ^-+^.i-((?!"`:>l<[~<]nQY+?????][{\cmO||l . ''.. + '|: '^[{~)\_+++))1{uxnvt(t){{[[u0\1|({1()){-?|xfc: .. + .<-, ]-]]]})11)){{{{}{{)|{}}{1{111{11{{}}]_!"x\]Xf + ,]<\}][[[[[[[[[[[[[[[]][[[[[[[[][[[[[[]?-+!YC{z} .` + "_[}?]][[[[[[[[[[[[[[[[[[][[[[[[[[[[[[[[[]vn\?. ^ + ^. ;{_(_??][[[[[[[[[[[[[[[[[|[[[[[[[[[[[[[[[[v_(]^ + .' '. :t>/?[[[[[[[[[[[[[[[[[[[]t\][[[[[[[[[[][]?u;()_ .. .'. . . + `. .` :)!j_]][[[]]]]][[[[[[[[[[}j(/{[[[[[][[}1{~n!)ft . .. .. . .. . + ". ..' .` . "' .^":;~ti{\1]][]]]??]]]]][]]??[){[}[[[[?+}]!^':``^``,`;I.!<>?>:??i:;-;,<_..^,Ii: 'l,i+```' ..'!; ''. ?~.'<+ .li!:,1?}[[[[[[[[[[[[[[[[[[[[)1}]t[:, "O" . .' . ^!' . ". i+' . +^. . .':`'`~/1,-<-~^'^'^^,`.."i_>^. `1t]!,^I]l^;`,I::_?]?[!:;`.`"'`l!l<1f{~>;]\1(]I>~l!l[<,,;`lI,~},^>!>l'...'": 'x' ]l>i .:I1[~]]]]}}}}}[[}}[}}}}}[[]]]??1}}[}~;:>vx. :;..:??,.' ^` I;>.";:"^' .. ^'.^"" ' .!},' . +~!;:!".":i"^_/|]^li(\1;;it{' .[\fft+<}(/{}/)|f||'.^{/[)?!:(?+-,I+fjtil"'"+fj{i:',!!;!!^:.`r. r; !l'"i1?!i>~+_?][[[?-???]][[]?-+-]??+~<_{[_l?> ''^l;l`-}<`^.i>+l ``;I":+?!~-l ,>>l.'.;. ':!!(/!":,I +/t1ffft+{jff/ttff)];)?1(/tt\/t/tfttttfftf/1\|t\|/?<_]_]{<_]/f({fffjttf/[i>1//|tft|" :<~:+}, ]>if" .:-~ >) ^`^l)f(_{/\}-+1\()t-{j/]!:^'l<]\)+ ."_?I_{ +ffft)|)(t[_-{tjjrjrj/{(||}(rjj\1)I<\((ffj/rjffttjffftrjfffrtfff/f[1jjffffftt//)}tttff/ttt[<{rj}tf1?<:~{/j)>)fttf|?)tfffftt1_;+tf1-1|~i1, >;:} '1_ ;( .. .. `:"_1{}tjtvj)vjr/|jfff/<(tf)+1/)1j)~~-[j[l|[(/\j{:-]]([}\t diff --git a/src/main/scala/cc/sukazyo/cono/morny/Log.scala b/src/main/scala/cc/sukazyo/cono/morny/Log.scala new file mode 100644 index 0000000..5ef60b3 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/Log.scala @@ -0,0 +1,29 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.messiva.appender.ConsoleAppender +import cc.sukazyo.messiva.formatter.SimpleFormatter +import cc.sukazyo.messiva.log.LogLevel +import cc.sukazyo.messiva.logger.Logger + +import java.io.{PrintWriter, StringWriter} + +object Log { + + val logger: Logger = Logger( + ConsoleAppender( + SimpleFormatter() + ) + ).minLevel(LogLevel.INFO) + + def debug: Boolean = logger.levelSetting.minLevel.level <= LogLevel.DEBUG.level + + def debug(is: Boolean): Unit = + if is then logger.minLevel(LogLevel.ALL) + else logger.minLevel(LogLevel.INFO) + + def exceptionLog (e: Throwable): String = + val stackTrace = StringWriter() + e printStackTrace PrintWriter(stackTrace) + stackTrace toString + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala new file mode 100644 index 0000000..9d608ed --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny + +import java.io.IOException + +object MornyAbout { + + val MORNY_PREVIEW_IMAGE_ASCII: String = + try { MornyAssets.pack getResource "texts/server-hello.txt" readAsString } + catch case e: IOException => + throw RuntimeException("Cannot read MORNY_PREVIEW_IMAGE_ASCII from assets pack", e) + + val MORNY_SOURCECODE_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono" + val MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK = "https://storage.sukazyo.cc/Eyre_S/Coeur-Morny-Cono" + val MORNY_ISSUE_TRACKER_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono/issues" + val MORNY_USER_GUIDE_LINK = "https://book.sukazyo.cc/morny" + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala new file mode 100644 index 0000000..c331ea9 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala @@ -0,0 +1,11 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.restools.ResourcesPackage + +object MornyAssets { + + class AssetsException (caused: Throwable) extends Exception("Cannot read assets file.", caused) + + val pack: ResourcesPackage = ResourcesPackage(MornyAssets.getClass, "assets_morny") + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala new file mode 100644 index 0000000..9d0f2ba --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -0,0 +1,156 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.bot.command.MornyCommands +import cc.sukazyo.cono.morny.daemon.MornyDaemons +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.MornyCoeur.THREAD_SERVER_EXIT +import cc.sukazyo.cono.morny.bot.api.EventListenerManager +import cc.sukazyo.cono.morny.bot.event.{MornyEventListeners, MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock} +import cc.sukazyo.cono.morny.bot.query.MornyQueries +import com.pengrad.telegrambot.TelegramBot +import com.pengrad.telegrambot.request.GetMe + +import scala.util.boundary +import scala.util.boundary.break + +object MornyCoeur { + + val THREAD_SERVER_EXIT = "system-exit" + +} + +class MornyCoeur (using val config: MornyConfig) { + + given MornyCoeur = this + + ///>>> BLOCK START instance configure & startup stage 1 + + logger info "Coeur starting..." + + logger info s"args key:\n ${config.telegramBotKey}" + if config.telegramBotUsername ne null then + logger info s"login as:\n ${config.telegramBotUsername}" + + private val __loginResult: LoginResult = login() match + case some: Some[LoginResult] => some.get + case None => + logger error "Login to bot failed." + System exit -1 + throw RuntimeException() + + configure_exitCleanup() + + ///<<< BLOCK END instance configure & startup stage 1 + + /** [[TelegramBot]] account of this Morny */ + val account: TelegramBot = __loginResult.account + /** [[account]]'s telegram username */ + val username: String = __loginResult.username + /** [[account]]'s telegram user id */ + val userid: Long = __loginResult.userid + + /** current Morny's [[MornyTrusted]] instance */ + val trusted: MornyTrusted = MornyTrusted() + + val daemons: MornyDaemons = MornyDaemons() + //noinspection ScalaWeakerAccess + val eventManager: EventListenerManager = EventListenerManager() + eventManager register MornyOnUpdateTimestampOffsetLock() + val commands: MornyCommands = MornyCommands() + //noinspection ScalaWeakerAccess + val queries: MornyQueries = MornyQueries() + eventManager register MornyOnTelegramCommand(using commands) + eventManager register MornyOnInlineQuery(using queries) + //noinspection ScalaUnusedSymbol + val events: MornyEventListeners = MornyEventListeners(using eventManager) + + /** inner value: about why morny exit, used in [[daemon.MornyReport]]. */ + private var whileExit_reason: Option[AnyRef] = None + def exitReason: Option[AnyRef] = whileExit_reason + val coeurStartTimestamp: Long = ServerMain.systemStartupTime + + ///>>> BLOCK START instance configure & startup stage 2 + + daemons.start() + logger info "start telegram event listening" + account setUpdatesListener eventManager + if config.commandLoginRefresh then + logger info "resetting telegram command list" + commands.automaticTGListUpdate() + + daemons.reporter.reportCoeurMornyLogin() + logger info "Coeur start complete." + + ///<<< BLOCK END instance configure & startup stage 2 + + def saveDataAll(): Unit = { + // nothing to do + logger info "done all save action." + } + + private def exitCleanup (): Unit = { + daemons.reporter.reportCoeurExit() + account.shutdown() + logger info "stopped bot account" + daemons.stop() + if config.commandLogoutClear then + commands.automaticTGListRemove() + logger info "done exit cleanup" + } + + private def configure_exitCleanup (): Unit = { + Runtime.getRuntime.addShutdownHook(new Thread(() => exitCleanup(), THREAD_SERVER_EXIT)) + } + + def exit (status: Int, reason: AnyRef): Unit = + whileExit_reason = Some(reason) + System exit status + + private case class LoginResult(account: TelegramBot, username: String, userid: Long) + + private def login (): Option[LoginResult] = { + + val builder = TelegramBot.Builder(config.telegramBotKey) + var api_bot = config.telegramBotApiServer + var api_file = config.telegramBotApiServer4File + if (api_bot ne null) + if api_bot endsWith "/" then api_bot = api_bot dropRight 1 + if !(api_bot endsWith "/bot") then api_bot += "/bot" + builder.apiUrl(api_bot) + if (api_file ne null) + if api_file endsWith "/file/" then api_file = api_file dropRight 1 + if !(api_file endsWith "/file/bot") then api_file += "/file/bot" + builder.apiUrl(api_bot) + if ((api_bot ne null) || (api_file ne null)) + logger info + s"""Telegram bot api set to: + |- bot: $api_bot + |- file: $api_file""" + .stripMargin + + val account = builder build + + logger info "Trying to login..." + boundary[Option[LoginResult]] { + for (i <- 0 to 3) { + if i > 0 then logger info "retrying..." + try { + val remote = (account execute GetMe()).user + if ((config.telegramBotUsername ne null) && config.telegramBotUsername != remote.username) + throw RuntimeException(s"Required the bot @${config.telegramBotUsername} but @${remote.username} logged in") + logger info s"Succeed logged in to @${remote.username}" + break(Some(LoginResult(account, remote.username, remote.id))) + } catch + case r: boundary.Break[Option[LoginResult]] => throw r + case e => + logger error + s"""${exceptionLog(e)} + |login failed""" + .stripMargin + } + None + } + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java new file mode 100644 index 0000000..61e5826 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java @@ -0,0 +1,185 @@ +package cc.sukazyo.cono.morny; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.annotation.*; +import java.time.ZoneOffset; +import java.util.HashSet; +import java.util.Set; + +public class MornyConfig { + + /** + * 表示一个字段的值属于敏感数据,不应该被执行打印等操作。 + */ + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface Sensitive {} + + /* ======================================= * + * Config props Names Definition * + * ======================================= */ + + public static final String PROP_TOKEN_KEY_DEFAULT = "TELEGRAM_BOT_API_TOKEN"; + public static final String PROP_TOKEN_MORNY_KEY = "MORNY_TG_TOKEN"; + public static final String[] PROP_TOKEN_KEY = {PROP_TOKEN_KEY_DEFAULT, PROP_TOKEN_MORNY_KEY}; + + /* ======================================= * + * telegram bot login config * + * ======================================= */ + + /** + * Morny Telegram 使用的 API 服务器. + *

+ * 不设定的话,默认将会使用 {@code https://api.telegram.org/bot} + */ + @Nullable public final String telegramBotApiServer; + /** + * Morny Telegram 使用的 API 服务器的 file 服务路径. + *

+ * 不设定的话,默认将会使用 {@value com.pengrad.telegrambot.impl.FileApi#FILE_API} + */ + @Nullable public final String telegramBotApiServer4File; + + /** + * morny 使用的 telegram bot 的 bot api token. + *

+ * 这个值必须设定。 + */ + @Nonnull @Sensitive public final String telegramBotKey; + /** + * morny 所使用的 bot 的 username. + *

+ * 如果设定了这个值,则在 morny 登录 bot 时将会检查所登录的 bot 的 username 是否和这里设定的 username 匹配。 + * 如果不匹配,则会拒绝登录然后报错。 + *

+ * 如果没有设定这个值,则不会对登录 bot 的 username 进行限制。 + */ + @Nullable public final String telegramBotUsername; + + /* ======================================= * + * morny trusted config * + * ======================================= */ + + /** + * morny 的主人. + *

+ * 这项值的对象总是会被{@link MornyTrusted 信任管理器}认为是可信任的 + */ + public final long trustedMaster; + /** + * morny 可信群聊的 id. + *

+ * {@link MornyTrusted 信任管理器}将会认为这个群聊中的所有拥有 + * {@link com.pengrad.telegrambot.model.ChatMember.Status#administrator administrator} 权限的成员是可信任的。 + *

+ * id 需要符合 bot api 标准。 + */ + public final long trustedChat; + + /* ======================================= * + * system: event ignore * + * ======================================= */ + + public final boolean eventIgnoreOutdated; + /** + * morny 的事件忽略前缀时间
+ *
+ * {@link cc.sukazyo.cono.morny.bot.event.MornyOnUpdateTimestampOffsetLock} + * 会根据这里定义的时间戳取消掉比此时间更早的事件链 + */ + public final long eventOutdatedTimestamp; + + /* ======================================= * + * system: command list automation * + * ======================================= */ + + public final boolean commandLoginRefresh; + public final boolean commandLogoutClear; + + /* ======================================= * + * system: morny report * + * ======================================= */ + + /** + * 控制 Morny Coeur 系统的报告的报告对象. + * @since 1.0.0-alpha5 + */ + public final long reportToChat; + + /* ======================================= * + * function: dinner query tool * + * ======================================= */ + + @Nonnull public final Set dinnerTrustedReaders; + public final long dinnerChatId; + + /* ======================================= * + * function: medication timer * + * ======================================= */ + + public final long medicationNotifyToChat; + + @Nonnull public final ZoneOffset medicationTimerUseTimezone; + + @Nonnull public final Set medicationNotifyAt; + + /* ======================================= * + * End Configs | ConfigBuilder * + * ======================================= */ + + private MornyConfig (@Nonnull Prototype prototype) throws CheckFailure { + this.telegramBotApiServer = prototype.telegramBotApiServer; + this.telegramBotApiServer4File = prototype.telegramBotApiServer4File; + if (prototype.telegramBotKey == null) throw new CheckFailure.NullTelegramBotKey(); + this.telegramBotKey = prototype.telegramBotKey; + this.telegramBotUsername = prototype.telegramBotUsername; + this.trustedMaster = prototype.trustedMaster; + this.trustedChat = prototype.trustedChat; + this.eventIgnoreOutdated = prototype.eventIgnoreOutdated; + if (prototype.eventOutdatedTimestamp < 1) throw new CheckFailure.UnsetEventOutdatedTimestamp(); + this.eventOutdatedTimestamp = prototype.eventOutdatedTimestamp; + this.commandLoginRefresh = prototype.commandLoginRefresh; + this.commandLogoutClear = prototype.commandLogoutClear; + this.dinnerTrustedReaders = prototype.dinnerTrustedReaders; + this.dinnerChatId = prototype.dinnerChatId; + this.reportToChat = prototype.reportToChat; + this.medicationNotifyToChat = prototype.medicationNotifyToChat; + this.medicationTimerUseTimezone = prototype.medicationTimerUseTimezone; + prototype.medicationNotifyAt.forEach(i -> { if (i < 0 || i > 23) throw new CheckFailure.UnavailableTimeInMedicationNotifyAt(); }); + this.medicationNotifyAt = prototype.medicationNotifyAt; + } + + public static class CheckFailure extends RuntimeException { + public static class NullTelegramBotKey extends CheckFailure {} + public static class UnsetEventOutdatedTimestamp extends CheckFailure {} + public static class UnavailableTimeInMedicationNotifyAt extends CheckFailure {} + } + + public static class Prototype { + + public MornyConfig build () { + return new MornyConfig(this); + } + + @Nullable public String telegramBotApiServer = null; + @Nullable public String telegramBotApiServer4File = null; + @Nullable public String telegramBotKey = null; + @Nullable public String telegramBotUsername = null; + public long trustedMaster = -1L; + public long trustedChat = -1L; + public boolean eventIgnoreOutdated = false; + public long eventOutdatedTimestamp = -1; + public boolean commandLoginRefresh = false; + public boolean commandLogoutClear = false; + @Nonnull public final Set dinnerTrustedReaders = new HashSet<>(); + public long dinnerChatId = -1L; + public long reportToChat = -1L; + public long medicationNotifyToChat = -1L; + @Nonnull public ZoneOffset medicationTimerUseTimezone = ZoneOffset.UTC; + @Nonnull public final Set medicationNotifyAt = new HashSet<>(); + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala new file mode 100644 index 0000000..97866db --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala @@ -0,0 +1,48 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.internal.BuildConfigField +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.util.FileUtils + +import java.io.IOException +import java.net.URISyntaxException +import java.security.NoSuchAlgorithmException + +object MornySystem { + + @BuildConfigField val VERSION: String = BuildConfig.VERSION + @BuildConfigField val VERSION_FULL: String = BuildConfig.VERSION_FULL + @BuildConfigField val VERSION_BASE: String = BuildConfig.VERSION_BASE + @BuildConfigField val VERSION_DELTA: String = BuildConfig.VERSION_DELTA + @BuildConfigField val CODENAME: String = BuildConfig.CODENAME + @BuildConfigField val CODE_STORE: String = BuildConfig.CODE_STORE + //noinspection ScalaWeakerAccess + @BuildConfigField val COMMIT_PATH: String = BuildConfig.COMMIT_PATH + + @BuildConfigField + def isUseDelta: Boolean = VERSION_DELTA ne null + + @BuildConfigField + def isGitBuild: Boolean = BuildConfig.COMMIT ne null + + @BuildConfigField + def isCleanBuild: Boolean = BuildConfig.CLEAN_BUILD + + def currentCodePath: String|Null = + if ((COMMIT_PATH eq null) || (!isGitBuild)) null + else COMMIT_PATH.formatted(BuildConfig.COMMIT) + + def getJarMD5: String = { + try { + FileUtils.getMD5Three(MornySystem.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.getPath) + } catch + //noinspection ScalaUnnecessaryParentheses + case _: (IOException|URISyntaxException) => + "" + case n: NoSuchAlgorithmException => + logger error exceptionLog(n) +// MornyReport.exception(n, "") // todo: will not implemented + "" + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala new file mode 100644 index 0000000..1400db6 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala @@ -0,0 +1,24 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.{LimboChat, LimboUser} +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Chat.* +import cc.sukazyo.cono.morny.Log.logger +import com.pengrad.telegrambot.model.ChatMember.Status +import com.pengrad.telegrambot.TelegramBot + +class MornyTrusted (using coeur: MornyCoeur)(using config: MornyConfig) { + + if config.trustedMaster == -1 then + logger warn "You have not set your Morny's master.\n it may have some issues on controlling your bot." + + def isTrusted (userId: Long): Boolean = + given TelegramBot = coeur.account + if userId == config.trustedMaster then true + else if config.trustedChat == -1 then false + else LimboChat(config.trustedChat) memberHasPermission(LimboUser(userId), Status.administrator) + + def isTrusted_dinnerReader (userId: Long): Boolean = + if userId == config.trustedMaster then true + else config.dinnerTrustedReaders contains userId + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala new file mode 100644 index 0000000..5947797 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala @@ -0,0 +1,156 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyConfig.CheckFailure +import cc.sukazyo.cono.morny.util.CommonFormat + +import java.time.ZoneOffset +import scala.collection.mutable.ArrayBuffer +import scala.language.postfixOps + +object ServerMain { + + private val THREAD_MORNY_INIT: String = "morny-init" + + val systemStartupTime: Long = System.currentTimeMillis() + + def main (args: Array[String]): Unit = { + + val config = new MornyConfig.Prototype() + var mode_echoVersion = false + var mode_echoHello = false + var showHello = true + + config.eventOutdatedTimestamp = systemStartupTime + + val unknownArgs = ArrayBuffer[String]() + + var i = 0 + while (i < args.length) { + args(i) match { + + case "-d" | "--dbg" | "--debug" => Log.debug(true) + + case "--no-hello" | "-hf" | "--quiet" | "-q" => showHello = false + case "--only-hello" | "-ho" | "-o" | "-hi" => mode_echoHello = true + case "--version" | "-v" => mode_echoVersion = true + + case "--outdated-block" | "-ob" => config.eventIgnoreOutdated = true + + case "--api" | "-a" => i+=1 ; config.telegramBotApiServer = args(i) + case "--api-files" | "files-api" | "-af" => i+=1; config.telegramBotApiServer4File = args(i) + + case "--token" | "-t" => i+=1 ; config.telegramBotKey = args(i) + case "--username" | "-u" => i+=1 ; config.telegramBotUsername = args(i) + + case "--master" | "-mm" => i+=1 ; config.trustedMaster = args(i)toLong + case "--trusted-chat" | "-trs" => i+=1 ; config.trustedChat = args(i)toLong + case "--report-to" => i+=1; config.reportToChat = args(i)toLong + + case "--trusted-reader-dinner" | "-trsd" => i+=1 ; config.dinnerTrustedReaders add (args(i)toLong) + case "--dinner-chat" | "-chd" => i+=1 ; config.dinnerChatId = args(i)toLong + + case "--medication-notify-chat" | "-medc" => i+=1 ; config.medicationNotifyToChat = args(i)toLong + case "--medication-notify-timezone" | "-medtz" => + i+=1 + config.medicationTimerUseTimezone = ZoneOffset.ofHours(args(i)toInt) + case "--medication-notify-times" | "-medt" => + i+=1 + for (u <- args(i) split ",") { + config.medicationNotifyAt add (u toInt) + } + + case "--auto-cmd-list" | "-ca" => config.commandLoginRefresh = true + case "--auto-cmd-remove" | "-cr" => config.commandLogoutClear = true + case "--auto-cmd" | "-cmd" | "-c" => + config.commandLoginRefresh = true + config.commandLogoutClear = true + + case _ => unknownArgs append args(i) + + } + i+=1 + } + + /// Setup launch params from ENVIRONMENT + var propToken: String = null + var propTokenKey: String = null + for (iKey <- MornyConfig.PROP_TOKEN_KEY) { + if ((System getenv iKey) != null) { + propToken = System getenv iKey + propTokenKey = iKey + } + } + + /// + /// Output startup message + /// process startup params - like startup mode + /// + + if (showHello) logger info MornyAbout.MORNY_PREVIEW_IMAGE_ASCII + if (mode_echoHello) return; + + if (unknownArgs.nonEmpty) logger warn + s"""Can't understand arg to some meaning + | ${unknownArgs mkString "\n "}""" + .stripMargin + + if (Log debug) + logger warn + """Debug log output enabled. + | It may lower your performance, make sure that you are not in production environment.""" + .stripMargin + + if (mode_echoVersion) { + + logger info + s"""Morny Cono Version + |- version : + | Morny ${MornySystem.CODENAME toUpperCase} + | ${MornySystem.VERSION_BASE}${if (MornySystem.isUseDelta) "-δ"+MornySystem.VERSION_DELTA else ""} + |- md5hash : + | ${MornySystem.getJarMD5} + |- gitstat : + |${ if (MornySystem.isGitBuild) { + s""" on commit ${if (MornySystem.isCleanBuild) "- clean-build" else "<δ/non-clean-build>"} + | ${BuildConfig.COMMIT}""" + .stripMargin + } else " "} + |- buildtd : + | ${BuildConfig.CODE_TIMESTAMP} + | ${CommonFormat.formatDate(BuildConfig.CODE_TIMESTAMP, 0)} [UTC]""" + .stripMargin + return + + } + + logger info + s"""ServerMain.java Loaded >>> + |- version ${MornySystem.VERSION_FULL} + |- Morny ${MornySystem.CODENAME toUpperCase} + |- <${MornySystem.getJarMD5}> [${BuildConfig.CODE_TIMESTAMP}]""".stripMargin + + /// + /// Check Coeur arguments + /// finally start Coeur Program + /// + + if (propToken != null) { + config.telegramBotKey = propToken + logger info s"Parameter set by EnvVar $$$propTokenKey" + } + + Thread.currentThread setName THREAD_MORNY_INIT + + try + MornyCoeur(using config build) + catch { + case _: CheckFailure.NullTelegramBotKey => + logger.info("Parameter required has no value:\n --token.") + case e: CheckFailure => + logger.error("Unknown failure occurred while starting ServerMain!:") + e.printStackTrace(System.out) + } + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala new file mode 100644 index 0000000..0a3e1ac --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala @@ -0,0 +1,22 @@ +package cc.sukazyo.cono.morny.bot.api + +import com.pengrad.telegrambot.model.Update + +trait EventListener () { + + def onMessage (using Update): Boolean = false + def onEditedMessage (using Update): Boolean = false + def onChannelPost (using Update): Boolean = false + def onEditedChannelPost (using Update): Boolean = false + def onInlineQuery (using Update): Boolean = false + def onChosenInlineResult (using Update): Boolean = false + def onCallbackQuery (using Update): Boolean = false + def onShippingQuery (using Update): Boolean = false + def onPreCheckoutQuery (using Update): Boolean = false + def onPoll (using Update): Boolean = false + def onPollAnswer (using Update): Boolean = false + def onMyChatMemberUpdated (using Update): Boolean = false + def onChatMemberUpdated (using Update): Boolean = false + def onChatJoinRequest (using Update): Boolean = false + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala new file mode 100644 index 0000000..5448204 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala @@ -0,0 +1,105 @@ +package cc.sukazyo.cono.morny.bot.api + +import cc.sukazyo.cono.morny.{Log, MornyCoeur} +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException +import com.google.gson.GsonBuilder +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.UpdatesListener + +import scala.collection.mutable +import scala.language.postfixOps + +/** Contains a [[mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]]. + * + * Implemented [[process]] in [[UpdatesListener]] so it can directly used in [[com.pengrad.telegrambot.TelegramBot.setupListener]]. + * + * @param coeur the [[MornyCoeur]] context. + */ +class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener { + + private val listeners = mutable.Queue.empty[EventListener] + + def register (listeners: EventListener*): Unit = + this.listeners ++= listeners + + private class EventRunner (using event: Update) extends Thread { + this setName s"evt-${event.updateId()}-nn" + private def updateThreadName (t: String): Unit = + this setName s"evt-${event.updateId()}-$t" + + override def run (): Unit = { + for (i <- listeners) { + object status: + var _status = 0 + def isOk: Boolean = _status > 0 + def check (u: Boolean): Unit = if u then _status = _status + 1 + try { + updateThreadName("message") + if event.message ne null then status check i.onMessage + updateThreadName("edited-message") + if event.editedMessage ne null then status check i.onEditedMessage + updateThreadName("channel-post") + if event.channelPost ne null then status check i.onChannelPost + updateThreadName("edited-channel-post") + if event.editedChannelPost ne null then status check i.onEditedChannelPost + updateThreadName("inline-query") + if event.inlineQuery ne null then status check i.onInlineQuery + updateThreadName("chosen-inline-result") + if event.chosenInlineResult ne null then status check i.onChosenInlineResult + updateThreadName("callback-query") + if event.callbackQuery ne null then status check i.onCallbackQuery + updateThreadName("shipping-query") + if event.shippingQuery ne null then status check i.onShippingQuery + updateThreadName("pre-checkout-query") + if event.preCheckoutQuery ne null then status check i.onPreCheckoutQuery + updateThreadName("poll") + if event.poll ne null then status check i.onPoll + updateThreadName("poll-answer") + if event.pollAnswer ne null then status check i.onPollAnswer + updateThreadName("my-chat-member") + if event.myChatMember ne null then status check i.onMyChatMemberUpdated + updateThreadName("chat-member") + if event.chatMember ne null then status check i.onChatMemberUpdated + updateThreadName("chat-join-request") + if event.chatJoinRequest ne null then status check i.onChatJoinRequest + } catch case e => { + val errorMessage = StringBuilder() + errorMessage ++= "Event throws unexpected exception:\n" + errorMessage ++= (exceptionLog(e) indent 4) + e match + case actionFailed: EventRuntimeException.ActionFailed => + errorMessage ++= "\ntg-api action: response track: " + errorMessage ++= (GsonBuilder().setPrettyPrinting().create().toJson( + actionFailed.response + ) indent 4) ++= "\n" + case _ => + logger error errorMessage.toString + coeur.daemons.reporter.exception(e, "on event running") + } + if (status isOk) return + } + } + + } + + + import java.util + import scala.jdk.CollectionConverters.* + /** Delivery the telegram [[Update]]s. + * + * The implementation of [[UpdatesListener]]. + * + * For each [[Update]], create an [[EventRunner]] for it, and + * start the it. + * + * @return [[UpdatesListener.CONFIRMED_UPDATES_ALL]], for all Updates + * should be processed in [[EventRunner]] created for it. + */ + override def process (updates: util.List[Update]): Int = { + for (update <- updates.asScala) + EventRunner(using update).start() + UpdatesListener.CONFIRMED_UPDATES_ALL + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala new file mode 100644 index 0000000..032482b --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala @@ -0,0 +1,59 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.{Chat, Update} +import com.pengrad.telegrambot.request.{DeleteMessage, GetChatMember, SendSticker} + +import scala.language.postfixOps + +class DirectMsgClear (using coeur: MornyCoeur) extends ISimpleCommand { + + override val name: String = "r" + override val aliases: Array[ICommandAlias] | Null = null + + override def execute (using command: InputCommand, event: Update): Unit = { + + logger debug "executing command /r" + if (event.message.replyToMessage == null) return; + logger trace "message is a reply" + if (event.message.replyToMessage.from.id != coeur.userid) return; + logger trace "message replied is from me" + if (System.currentTimeMillis/1000 - event.message.replyToMessage.date > 48*60*60) return; + logger trace "message is not outdated(48 hrs ago)" + + val isTrusted = coeur.trusted isTrusted event.message.from.id + // todo: + // it does not work. due to the Telegram Bot API doesn't provide + // nested replyToMessage, so currently the trusted check by + // replyToMessage.replyToMessage will not work! + def _isReplyTrusted: Boolean = + if (event.message.replyToMessage.replyToMessage == null) false + else if (event.message.replyToMessage.replyToMessage.from.id == event.message.from.id) true + else false + + if (isTrusted || _isReplyTrusted) { + + coeur.account exec DeleteMessage( + event.message.chat.id, event.message.replyToMessage.messageId + ) + + def _isPrivate: Boolean = event.message.chat.`type` == Chat.Type.Private + def _isPermission: Boolean = + (coeur.account exec GetChatMember(event.message.chat.id, event.message.from.id)) + .chatMember.canDeleteMessages + if (_isPrivate || _isPermission) { + coeur.account exec DeleteMessage(event.message.chat.id, event.message.messageId) + } + + } else coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(event.message.messageId) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala new file mode 100644 index 0000000..07b1c5a --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala @@ -0,0 +1,230 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.CommonEncrypt +import cc.sukazyo.cono.morny.util.CommonEncrypt.* +import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.{PhotoSize, Update} +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{GetFile, SendDocument, SendMessage, SendSticker} + +import java.io.IOException +import java.util.Base64 +import scala.language.postfixOps + +/** Provides Telegram Command __`/encrypt`__. */ +class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { + + override val name: String = "encrypt" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "[algorithm|(l)] [(uppercase)]" + override val description: String = "通过指定算法加密回复的内容 (目前只支持文本)" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val args = command.args + + // show a simple help page + if ((args isEmpty) || ((args(0) equals "l") && (args.length == 1))) + echoHelp(event.message.chat.id, event.message.messageId) + return + + // for mod-params: + // mod-params is the args belongs to the encrypt algorithm. + // due to the algorithm is defined in the 1st (array(0)) arg, + // so the mod-params is which defined since the 2nd arg. also + // due to there's only one mod-param yet (it is uppercase), + // so the algorithm will be and must be in the 2nd arg. + /** inner function: is input `arg` means mod-param ''uppercase'' */ + def _is_mod_u(arg: String): Boolean = + if (arg equalsIgnoreCase "uppercase") return true + if (arg equalsIgnoreCase "u") return true + if (arg equalsIgnoreCase "upper") return true + false + val mod_uppercase = if (args.length > 1) { + if (args.length < 3 && _is_mod_u(args(1))) true + else + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + return + } else false + + // BLOCK: get input + // for now, only support getting data from replied message, and + // this message CAN ONLY have texts or an universal file: if the + // universal files are not only one, only the first one can be get. + // - do NOT SUPPORT telegram inline image/video/autio yet + // - do NOT SUPPORT multi-file yet + // todo: support inline image/video/audio file and multi-files. + /** inner trait: the encryptable data abstract */ + trait XEncryptable { /** standards data to [[Array]]`[`[[Byte]]`]` for processing */ val asByteArray: Array[Byte] } + /** inner class: the [[XEncryptable]] implementation of binary([[Array]]`[`[[Byte]]`]`) data (file or something) */ + case class XFile (data: Array[Byte], name: String) extends XEncryptable: + val asByteArray: Array[Byte] = data + /** inner class: the [[XEncryptable]] implementation of [[String]] data */ + case class XText (data: String) extends XEncryptable: + val asByteArray: Array[Byte] = data getBytes CommonEncrypt.ENCRYPT_STANDARD_CHARSET + val input: XEncryptable = + val _r = event.message.replyToMessage + if ((_r ne null) && (_r.document ne null)) { + try {XFile( + coeur.account getFileContent (coeur.account exec GetFile(_r.document.fileId)).file, + _r.document.fileName + )} catch case e: IOException => + logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" + coeur.daemons.reporter.exception(e, "NetworkRequest error: TelegramFileAPI") + return + } else if ((_r ne null) && (_r.photo ne null)) { + try { + var _photo_origin: PhotoSize = null + var _photo_size: Long = 0 + for (size <- _r.photo) + val _size = (size.width longValue)*size.height + if (_photo_size < _size) + _photo_origin = size + _photo_size = _size + if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.") + import cc.sukazyo.cono.morny.util.UseRandom.rand_id + XFile( + coeur.account getFileContent (coeur.account exec GetFile(_photo_origin.fileId)).file, + s"photo$rand_id.png" + ) + } catch + case e: IOException => + //noinspection DuplicatedCode + logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" + coeur.daemons.reporter.exception(e, "NetworkRequest error: TelegramFileAPI") + return + case e: IllegalArgumentException => + logger warn s"FileProcess error: PhotoSize:\n\t${e.getMessage}" + coeur.daemons.reporter.exception(e, "FileProcess error: PhotoSize") + return + } else if ((_r ne null) && (_r.text ne null)) { + XText(_r.text) + } else { + coeur.account exec SendMessage( + event.message.chat.id, + "null" + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + return + } + // END BLOCK: get input + + // BLOCK: encrypt + /** inner class: encrypt result implementation of text-like (can be described as [[String]]). */ + trait EXTextLike { val text: String } + /** inner class: encrypt result implementation of a file */ + case class EXFile (result: Array[Byte], resultName: String) + /** inner class: [[EXTextLike]] implementation of just normal text */ + case class EXText (text: String) extends EXTextLike + /** inner class: [[EXTextLike]] implementation of a special type: hash value */ + case class EXHash (text: String) extends EXTextLike + /** generate encrypt result by making normal encrypt: output type == input type */ + def genResult_encrypt (source: XEncryptable, processor: Array[Byte]=>Array[Byte], filenameProcessor: String=>String): EXFile|EXText = { + source match + case x_file: XFile => EXFile(processor(x_file asByteArray), filenameProcessor(x_file.name)) + case x: XText => EXText(String(processor(x asByteArray), CommonEncrypt.ENCRYPT_STANDARD_CHARSET)) + } + /** generate encrypt result by making hash: output type == hash value */ + def genResult_hash (source: XEncryptable, processor: Array[Byte]=>Array[Byte]): EXHash = + val hashed = processor(source asByteArray) toHex; + EXHash(if mod_uppercase then hashed toUpperCase else hashed) + val result: EXHash|EXFile|EXText = args(0) match + case "base64" | "b64" | "base64url" | "base64u" | "b64u" => + val _tool_b64 = + if args(0) contains "u" then Base64.getUrlEncoder + else Base64.getEncoder + genResult_encrypt( + input, + _tool_b64.encode, + n => n+".b64.txt" + ) + case "base64decode" | "base64d" | "b64d" | "base64url-decode" | "base64ud" | "b64ud" => + val _tool_b64d = + if args(0) contains "u" then Base64.getUrlDecoder + else Base64.getDecoder + try { genResult_encrypt( + input, + _tool_b64d.decode, + CommonEncrypt.lint_base64FileName + ) } catch case _: IllegalArgumentException => + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 // todo: is here better erro notify? + ).replyToMessageId(event.message.messageId) + return + case "md5" => genResult_hash(input, MD5) + case "sha1" => genResult_hash(input, SHA1) + case "sha256" => genResult_hash(input, SHA256) + case "sha512" => genResult_hash(input, SHA512) + case _ => + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + return; + // END BLOCK: encrypt + + // output + result match + case _file: EXFile => + coeur.account exec SendDocument( + event.message.chat.id, + _file.result + ).fileName(_file.resultName).replyToMessageId(event.message.messageId) + case _text: EXTextLike => + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + coeur.account exec SendMessage( + event.message.chat.id, + s"

${h(_text.text)}
" + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + + } + + /** echo help to a specific message in a specific chat. + * + * === the help message === + * The first paragraph lists available encrypt algorithms and its alias, + * each line have one algorithm where the first name highlighted is the + * main name and following is aliases separated with `,`. + * with the separator `---`, the second paragraph lists available mods + * for algorithms, displays with the same rule of algorithms, with an extra + * italic text following describes its usage environment. + * + * when output to telegram just like: + *
+ * '''__base64__''', b64
+ * '''__base64url__''', base64u, b64u
+ * '''__base64decode__''', base64d, b64d
+ * '''__base64url-decode__''', base64ud, b64ud
+ * '''__sha1__'''
+ * '''__sha256__'''
+ * '''__sha512__'''
+ * '''__md5__'''
+ * ---
+ * '''__uppercase__''', upper, u ''(sha1/sha256/sha512/md5 only)'' + *
+ */ + private def echoHelp(chat: Long, replyTo: Int): Unit = + coeur.account exec SendMessage( + chat, + s"""base64, b64 + |base64url, base64u, b64u + |base64decode, base64d, b64d + |base64url-decode, base64ud, b64ud + |sha1 + |sha256 + |sha512 + |md5 + |--- + |uppercase, upper, u (sha1/sha256/sha512/md5 only)""" + .stripMargin + ).replyToMessageId(replyTo).parseMode(ParseMode HTML) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala new file mode 100644 index 0000000..03211e8 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala @@ -0,0 +1,57 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendSticker + +import scala.language.postfixOps + +class EventHack (using coeur: MornyCoeur) extends ITelegramCommand { + + override val name: String = "event_hack" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "[(user|group|any)]" + override val description: String = "输出 bot 下一个获取到的事件序列化数据" + + override def execute (using command: InputCommand, event: Update): Unit = { + + import coeur.daemons.eventHack.{registerHack, HackType} + + val x_mode = if (command.args nonEmpty) command.args(0) else "" + + def done_ok = + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_WAITING + ).replyToMessageId(event.message.messageId) + def done_forbiddenForAny = + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(event.message.messageId) + + def doRegister (t: HackType): Unit = + registerHack( + event.message.messageId longValue, + event.message.from.id, + event.message.chat.id, + t + ) + x_mode match + case "any" => + if (coeur.trusted isTrusted event.message.from.id) + doRegister(HackType ANY) + done_ok + else done_forbiddenForAny + case "group" => + doRegister(HackType GROUP) + done_ok + case _ => + doRegister(HackType USER) + done_ok + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala new file mode 100644 index 0000000..a960c07 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala @@ -0,0 +1,68 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.{InputCommand, Standardize} +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{GetChatMember, SendMessage} + +import scala.language.postfixOps + +class GetUsernameAndId (using coeur: MornyCoeur) extends ITelegramCommand { + + override val name: String = "user" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "[userid]" + override val description: String = "获取指定或回复的用户相关信息" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val args = command.args + + if (args.length > 1) + coeur.account exec SendMessage( + event.message.chat.id, + "[Unavailable] Too much arguments." + ).replyToMessageId(event.message.messageId) + return + + val userId: Long = + if (args nonEmpty) { + try args(0) toLong + catch case e: NumberFormatException => + coeur.account exec SendMessage( + event.message.chat.id, + s"[Unavailable] ${e.getMessage}" + ).replyToMessageId(event.message.messageId) + return + } else if (event.message.replyToMessage eq null) event.message.from.id + else event.message.replyToMessage.from.id + + val response = coeur.account execute GetChatMember(event.message.chat.id, userId) + + if (response.chatMember eq null) + coeur.account exec SendMessage( + event.message.chat.id, + "[Unavailable] user not found." + ).replyToMessageId(event.message.messageId) + return + + val user = response.chatMember.user + + if (user.id == Standardize.CHANNEL_SPEAKER_MAGIC_ID) + coeur.account exec SendMessage( + event.message.chat.id, + "$__channel_identify" + ).replyToMessageId(event.message.messageId) + return; + + coeur.account exec SendMessage( + event.message.chat.id, + TelegramUserInformation getFormattedInformation user + ).replyToMessageId(event.message.messageId()).parseMode(ParseMode HTML) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala new file mode 100644 index 0000000..de9dbe7 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala @@ -0,0 +1,44 @@ +package cc.sukazyo.cono.morny.bot.command + +/** One alias definition, contains the necessary message of how + * to process the alias. + */ +trait ICommandAlias { + + /** The alias name. + * + * same with the command name, it is the unique identifier of this alias. + */ + val name: String + /** If the alias should be listed while list commands to end-user. + * + * The alias can only be listed when the parent command can be listed + * (meanwhile the parent command implemented [[ITelegramCommand]]). If the + * parent command cannot be listed, it will always cannot be listed. + */ + val listed: Boolean + +} + +/** Default implementations of [[ICommandAlias]]. */ +object ICommandAlias { + + /** Alias which can be listed to end-user. + * + * the [[ICommandAlias.listed]] value is always true. + * + * @param name The alias name, see more in [[ICommandAlias.name]] + */ + case class ListedAlias (name: String) extends ICommandAlias: + override val listed = true + + /** Alias which cannot be listed to end-user. + * + * the [[ICommandAlias.listed]] value is always false. + * + * @param name The alias name, see more in [[ICommandAlias.name]] + */ + case class HiddenAlias (name: String) extends ICommandAlias: + override val listed = false + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala new file mode 100644 index 0000000..60d53a7 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala @@ -0,0 +1,78 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.ip186.IP186QueryHandler +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps + +class IP186Query (using coeur: MornyCoeur) { + + private enum Subs (val cmd: String): + case IP extends Subs("ip") + case WHOIS extends Subs("whois") + + object IP extends ITelegramCommand: + override val name: String = "ip" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "[ip]" + override val description: String = "通过 https://ip.186526.xyz 查询 ip 资料" + override def execute (using command: InputCommand, event: Update): Unit = query + object Whois extends ITelegramCommand: + override val name: String = "whois" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "[domain]" + override val description: String = "通过 https://ip.186526.xyz 查询域名资料" + override def execute (using command: InputCommand, event: Update): Unit = query + + private def query (using event: Update, command: InputCommand): Unit = { + + val target: String|Null = + if (command.args isEmpty) + if event.message.replyToMessage eq null then null else event.message.replyToMessage.text + else if (command.args.length > 1) + coeur.account exec SendMessage( + event.message.chat.id, + "[Unavailable] Too much arguments." + ).replyToMessageId(event.message.messageId) + return + else command.args(0) + + if (target eq null) + coeur.account exec new SendMessage( + event.message.chat.id, + "[Unavailable] No ip defined." + ).replyToMessageId(event.message.messageId) + return; + + + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + try { + + val response = command.command match + case Subs.IP.cmd => IP186QueryHandler.query_ip(target) + case Subs.WHOIS.cmd => IP186QueryHandler.query_whoisPretty(target) + case _ => throw IllegalArgumentException(s"Unknown 186-IP query method ${command.command}") + + coeur.account exec SendMessage( + event.message.chat.id, + s"""${h(response.url)} + |${h(response.body)}""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + + } catch case e: Exception => + coeur.account exec new SendMessage( + event.message().chat().id(), + s"""[Exception] in query: + |${h(e.getMessage)}""" + .stripMargin + ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId()) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala new file mode 100644 index 0000000..24c624f --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala @@ -0,0 +1,39 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update + +/** A simple command. + * + * Contains only [[name]] and [[aliases]]. + * + * Won't be listed to end-user. if you want the command listed, + * see [[ITelegramCommand]]. + * + */ +trait ISimpleCommand { + + /** the main name of the command. + * + * must have a value as the unique identifier of this command. + */ + val name: String + /** aliases of the command. + * + * Alias means it is the same to call [[name main name]] when call this. + * There can be multiple aliases. But notice that, although alias is not + * the unique identifier, it uses the same namespace with [[name]], means + * it also cannot be duplicate with other [[name]] or [[aliases]]. + * + * It can be [[Null]], means no aliases. + */ + val aliases: Array[ICommandAlias]|Null + + /** The work code of this command. + * + * @param command The parsed input command which called this command. + * @param event The raw event which called this command. + */ + def execute (using command: InputCommand, event: Update): Unit + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala new file mode 100644 index 0000000..1a720b8 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala @@ -0,0 +1,25 @@ +package cc.sukazyo.cono.morny.bot.command + +/** A complex telegram command. + * + * the extension of [[ISimpleCommand]], with external defines of the necessary + * introduction message ([[paramRule]] and [[description]]). + * + * It can be listed to end-user. + */ +trait ITelegramCommand extends ISimpleCommand { + + /** The param rule of this command, used in human-readable command list. + * + * The param rule uses a symbol language to describe how this command + * receives paras. + * + * Set it empty to make this scope not available. + */ + val paramRule: String + /** The description/introduction of this command, used in human-readable + * command list. + */ + val description: String + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala new file mode 100644 index 0000000..dbc3205 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala @@ -0,0 +1,123 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.{BotCommand, DeleteMyCommands, Update} +import com.pengrad.telegrambot.request.{SendSticker, SetMyCommands} + +import scala.collection.{mutable, SeqMap} +import scala.collection.mutable.ArrayBuffer +import scala.language.postfixOps + +class MornyCommands (using coeur: MornyCoeur) { + + private type CommandMap = SeqMap[String, ISimpleCommand] + private def CommandMap (commands: ISimpleCommand*): CommandMap = + val stash = mutable.SeqMap.empty[String, ISimpleCommand] + for (i <- commands) + stash += (i.name -> i) + if (i.aliases ne null) for (alias <- i.aliases) + stash += (alias.name -> i) + stash + + private val $MornyHellos = MornyHellos() + private val $IP186Query = IP186Query() + private val $MornyInformation = MornyInformation() + private val $MornyInformationOlds = MornyInformationOlds(using $MornyInformation) + private val $MornyManagers = MornyManagers() + //noinspection NonAsciiCharacters + private val $喵呜 = 喵呜() + private val commands: CommandMap = CommandMap( + + $MornyHellos.On, + $MornyHellos.Hello, + MornyInfoOnStart(), + GetUsernameAndId(), + EventHack(), + Nbnhhsh(), + $IP186Query.IP, + $IP186Query.Whois, + Encryptor(), + $MornyManagers.SaveData, + $MornyInformation, + $MornyInformationOlds.Version, + $MornyInformationOlds.Runtime, + MornyOldJrrp(), + $MornyManagers.Exit, + + Testing(), + DirectMsgClear(), + + //noinspection NonAsciiCharacters + 私わね(), + //noinspection NonAsciiCharacters + $喵呜.Progynova + + ) + + //noinspection NonAsciiCharacters + val commands_uni: CommandMap = CommandMap( + $喵呜.抱抱, + $喵呜.揉揉, + $喵呜.贴贴, + $喵呜.蹭蹭 + ) + + def execute (using command: InputCommand, event: Update): Boolean = { + if (commands contains command.command) + commands(command.command) execute; + true + else nonCommandExecutable + } + + private def nonCommandExecutable (using command: InputCommand, event: Update): Boolean = { + if command.target eq null then false + else + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + true + } + + def automaticTGListUpdate (): Unit = { + val listing = commands_toTelegramList + automaticTGListRemove() + coeur.account exec SetMyCommands(listing:_*) + logger info + s"""automatic updated telegram command list : + |${commandsTelegramList_toString(listing)}""".stripMargin + } + + def automaticTGListRemove (): Unit = { + coeur.account exec DeleteMyCommands() + logger info "cleaned up command list" + } + + private def commandsTelegramList_toString (list: Array[BotCommand]): String = + val builder = StringBuilder() + for (single <- list) + builder ++= s"${single.command} - ${single.description}\n" + (builder dropRight 1) toString + + private def commands_toTelegramList: Array[BotCommand] = + val list = ArrayBuffer.empty[BotCommand] + for ((name, command) <- commands) command match + case telegramCommand: ITelegramCommand if name == command.name => + list ++= formatTelegramCommandListLine(telegramCommand) + case _ => + list toArray + + private def formatTelegramCommandListLine (command: ITelegramCommand): Array[BotCommand] = + def buildOne (name: String, paramRule: String, intro: String): BotCommand = + BotCommand(name, if paramRule isBlank then intro else s"$paramRule - $intro") + val list = mutable.ArrayBuffer[BotCommand]( + buildOne(command.name, command.paramRule, command.description)) + if (command.aliases ne null) for (alias <- command.aliases) + if (alias.listed) list += buildOne(alias.name, "", "↑") + list toArray + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala new file mode 100644 index 0000000..34d8943 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala @@ -0,0 +1,44 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.command.ICommandAlias.ListedAlias +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendSticker + +import scala.language.postfixOps + +class MornyHellos (using coeur: MornyCoeur) { + + object On extends ITelegramCommand { + + override val name: String = "on" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "" + override val description: String = "检查是否在线" + + override def execute (using command: InputCommand, event: Update): Unit = + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_ONLINE_STATUS_RETURN + ).replyToMessageId(event.message.messageId) + + } + + object Hello extends ITelegramCommand { + + override val name: String = "hello" + override val aliases: Array[ICommandAlias] | Null = Array(ListedAlias("hi")) + override val paramRule: String = "" + override val description: String = "打招呼" + + override def execute (using command: InputCommand, event: Update): Unit = + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_HELLO + ).replyToMessageId(event.message.messageId) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala new file mode 100644 index 0000000..db0bd7e --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala @@ -0,0 +1,37 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.MornyInformation.{getAboutPic, getMornyAboutLinksHTML} +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendPhoto + +import scala.language.postfixOps + +class MornyInfoOnStart (using coeur: MornyCoeur) extends ISimpleCommand { + + override val name: String = "start" + override val aliases: Array[ICommandAlias] | Null = null + + override def execute (using command: InputCommand, event: Update): Unit = { + + coeur.account exec new SendPhoto( + event.message.chat.id, + getAboutPic + ).caption( + s"""欢迎使用 Morny Cono来自安妮的侍从小鼠。 + |Morny 具有各种各样的功能。 + | + |———————————————— + |$getMornyAboutLinksHTML + |———————————————— + | + |(你可以随时通过 /info 重新获得这些信息)""" + .stripMargin + ).parseMode(ParseMode HTML) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala new file mode 100644 index 0000000..36222eb --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -0,0 +1,153 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.{BuildConfig, MornyCoeur, MornySystem} +import cc.sukazyo.cono.morny.data.MornyInformation.* +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker} + +import java.lang.System +import scala.language.postfixOps + +// todo: maybe move some utils method outside +class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { + + private case object Subs { + val STICKERS = "stickers" + val RUNTIME = "runtime" + val VERSION = "version" + val VERSION_2 = "v" + } + + override val name: String = "info" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "[(version|runtime|stickers[.IDs])]" + override val description: String = "输出当前 Morny 的各种信息" + + override def execute (using command: InputCommand, event: Update): Unit = { + + if (command.args isEmpty) { + echoInfo(event.message.chat.id, event.message.messageId) + return + } + + val action: String = command.args(0) + + action match { + case s if s startsWith Subs.STICKERS => echoStickers + case Subs.RUNTIME => echoRuntime + case Subs.VERSION | Subs.VERSION_2 => echoVersion + case _ => echo404 + } + + } + + private def echoInfo (chatId: Long, replyTo: Int): Unit = { + coeur.account exec new SendPhoto( + chatId, + getAboutPic + ).caption( + s"""Morny Cono + |来自安妮的侍从小鼠。 + |———————————————— + |$getMornyAboutLinksHTML""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(replyTo) + } + + private def echoStickers (using command: InputCommand, event: Update): Unit = { + val mid: String|Null = + if (command.args(0) == Subs.STICKERS) { + if (command.args.length == 1) "" + else if (command.args.length == 2) command.args(1) + else null + } else if (command.args.length == 1) { + if ((command.args(0) startsWith s"${Subs.STICKERS}.") || (command.args(0) startsWith s"${Subs.STICKERS}#")) { + command.args(0) substring Subs.STICKERS.length+1 + } else null + } else null + if (mid == null) echo404 + else echoStickers(mid)(using event.message.chat.id, event.message.messageId) + } + + private def echoStickers (mid: String)(using send_chat: Long, send_replyTo: Int)(using Update): Unit = { + import scala.jdk.CollectionConverters.* + if (mid isEmpty) for ((_key, _file_id) <- TelegramStickers.map asScala) + echoSticker(_key, _file_id) + else { + try { + val sticker = TelegramStickers getById mid + echoSticker(sticker.getKey, sticker.getValue) + } catch case _: NoSuchFieldException => { + echo404 + } + } + } + + private def echoSticker (mid: String, file_id: String)(using send_chat: Long, send_replyTo: Int): Unit = { + val send_mid = SendMessage(send_chat, mid) + val send_sticker = SendSticker(send_chat, file_id) + if (send_replyTo != -1) send_mid.replyToMessageId(send_replyTo) + val result_send_mid = coeur.account exec send_mid + send_sticker.replyToMessageId(result_send_mid.message.messageId) + coeur.account exec send_sticker + } + + private[command] def echoVersion (using event: Update): Unit = { + val versionDeltaHTML = if (MornySystem.isUseDelta) s"-δ${h(MornySystem.VERSION_DELTA)}" else "" + val versionGitHTML = if (MornySystem.isGitBuild) s"git $getVersionGitTagHTML" else "" + coeur.account exec new SendMessage( + event.message.chat.id, + // language=html + s"""version: + |- Morny ${h(MornySystem.CODENAME toUpperCase)} + |- ${h(MornySystem.VERSION_BASE)}$versionDeltaHTML${if (MornySystem.isGitBuild) "\n- " + versionGitHTML else ""} + |coeur md5_hash: + |- ${h(MornySystem.getJarMD5)} + |coding timestamp: + |- ${BuildConfig.CODE_TIMESTAMP} + |- ${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC] + |""".stripMargin + ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) + } + + private[command] def echoRuntime (using event: Update): Unit = { + def sysprop (p: String): String = System.getProperty(p) + coeur.account exec new SendMessage( + event.message.chat.id, + /* language=html */ + s"""system: + |- ${h(if getRuntimeHostname nonEmpty then getRuntimeHostname.get else "")} + |- ${h(sysprop("os.name"))} ${h(sysprop("os.arch"))} ${h(sysprop("os.version"))} + |java runtime: + |- ${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))} + |- ${h(sysprop("java.vm.version"))} + |vm memory: + |- ${Runtime.getRuntime.totalMemory/1024/1024} / ${Runtime.getRuntime.maxMemory/1024/1024} MB + |- ${Runtime.getRuntime.availableProcessors} cores + |coeur version: + |- $getVersionAllFullTagHTML + |- ${h(MornySystem.getJarMD5)} + |- ${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC] + |- [${BuildConfig.CODE_TIMESTAMP}] + |continuous: + |- ${h(formatDuration(System.currentTimeMillis - coeur.coeurStartTimestamp))} + |- [${System.currentTimeMillis - coeur.coeurStartTimestamp}] + |- ${h(formatDate(coeur.coeurStartTimestamp, 0))} + |- [${coeur.coeurStartTimestamp}]""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + } + + private def echo404 (using event: Update): Unit = + coeur.account exec new SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala new file mode 100644 index 0000000..60f94ec --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala @@ -0,0 +1,18 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update + +class MornyInformationOlds (using base: MornyInformation) { + + object Version extends ISimpleCommand: + override val name: String = "version" + override val aliases: Array[ICommandAlias] | Null = null + override def execute (using command: InputCommand, event: Update): Unit = base.echoVersion + + object Runtime extends ISimpleCommand: + override val name: String = "runtime" + override val aliases: Array[ICommandAlias] | Null = null + override def execute (using command: InputCommand, event: Update): Unit = base.echoRuntime + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala new file mode 100644 index 0000000..5a143c0 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala @@ -0,0 +1,87 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.bot.command.ICommandAlias.HiddenAlias +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.daemon.MornyReport +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendSticker + +import scala.language.postfixOps + +class MornyManagers (using coeur: MornyCoeur) { + + object Exit extends ITelegramCommand { + + override val name: String = "exit" + override val aliases: Array[ICommandAlias] | Null = Array(HiddenAlias("stop"), HiddenAlias("quit")) + override val paramRule: String = "exit" + override val description: String = "关闭 Bot (仅可信成员)" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val user = event.message.from + + if (coeur.trusted isTrusted user.id) { + + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_EXIT + ).replyToMessageId(event.message.messageId) + logger info s"Morny exited by user ${user toLogTag}" + coeur.exit(0, user) + + } else { + + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(event.message.messageId) + logger info s"403 exit caught from user ${user toLogTag}" + coeur.daemons.reporter.unauthenticatedAction("/exit", user) + + } + + } + + } + + object SaveData extends ITelegramCommand { + + override val name: String = "save" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "" + override val description: String = "保存缓存数据到文件(仅可信成员)" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val user = event.message.from + + if (coeur.trusted isTrusted user.id) { + + logger info s"call save from command by ${user toLogTag}" + coeur.saveDataAll() + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_SAVED + ).replyToMessageId(event.message.messageId) + + } else { + + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(event.message.messageId) + logger info s"403 save caught from user ${user toLogTag}" + coeur.daemons.reporter.unauthenticatedAction("/save", user) + + } + + } + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala new file mode 100644 index 0000000..2d44bff --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala @@ -0,0 +1,36 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.data.MornyJrrp +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +class MornyOldJrrp (using coeur: MornyCoeur) extends ITelegramCommand { + + override val name: String = "jrrp" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "" + override val description: String = "获取 (假的) jrrp" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val user = event.message.from + val jrrp = MornyJrrp.jrrp_of_telegramUser(user, System.currentTimeMillis) + val ending = jrrp match + case s if s > 70 => "!" + case a if a > 30 => ";" + case _ => "..." + + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + coeur.account exec SendMessage( + event.message.chat.id, + // language=html + f"${user.fullnameRefHTML} 在(utc的)今天的运气指数是———— $jrrp%.2f%% ${h(ending)}" + ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala new file mode 100644 index 0000000..543a783 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -0,0 +1,85 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.{NbnhhshQuery, TelegramStickers} +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{SendMessage, SendSticker} + +import java.io.IOException +import scala.language.postfixOps + +class Nbnhhsh (using coeur: MornyCoeur) extends ITelegramCommand { + + private val NBNHHSH_RESULT_HEAD_HTML = + // language=html + "## Result of nbnhhsh query :" + + override val name: String = "nbnhhsh" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "[text]" + override val description: String = "检索文本内 nbnhhsh 词条" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val queryTarget: String = + if command.args nonEmpty then + command.args mkString " " + else if (event.message.replyToMessage != null && event.message.replyToMessage.text != null) + event.message.replyToMessage.text + else + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + return; + + try { + + val queryResp = NbnhhshQuery sendGuess queryTarget + + val message = StringBuilder(NBNHHSH_RESULT_HEAD_HTML) + + import cc.sukazyo.cono.morny.Log.logger + logger debug s"**xx len=${queryResp.words.length}" + for (_word <- queryResp.words) { + logger debug "**exec" + val _use_trans = (_word.trans ne null) && (_word.trans nonEmpty) + val _use_inputting = (_word.inputting ne null) && (_word.inputting nonEmpty) + if (_use_trans || _use_inputting) + message ++= s"\n\n[[ ${h(_word.name)} ]]" + logger debug s"**used [${_word.name}]" + if (_use_trans) for (_trans <- _word.trans) + message ++= s"\n* ${h(_trans)}" + logger debug s"**used [${_word.name}] used `${_trans}``" + if (_use_inputting) + logger debug s"**used [${_word.name}] inputting" + if (_use_trans) + message += '\n' + message ++= " maybe:" + for (_inputting <- _word.inputting) + logger debug s"**used [${_word.name}] used-i ${_inputting}" + message ++= s"\n` ${h(_inputting)}" + logger debug s"**exec as ${_word.name}" + } + + coeur.account exec SendMessage( + event.message.chat.id, + message toString + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + + } catch case e: IOException => { + coeur.account exec SendMessage( + event.message.chat.id, + s"""[Exception] in query: + |${h(e.getMessage)} + |""".stripMargin + ) + } + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala new file mode 100644 index 0000000..b7a1393 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala @@ -0,0 +1,28 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import javax.annotation.{Nonnull, Nullable} +import scala.language.postfixOps + +class Testing (using coeur: MornyCoeur) extends ISimpleCommand { + + override val name: String = "test" + override val aliases: Array[ICommandAlias] | Null = null + + override def execute (using command: InputCommand, event: Update): Unit = { + + coeur.account exec new SendMessage( + event.message.chat.id, + // language=html + "Just a TEST command." + ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala new file mode 100644 index 0000000..996ab79 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala @@ -0,0 +1,68 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.{Message, Update} +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{SendMessage, SendSticker} + +import javax.swing.text.html.HTML +import scala.annotation.unused +import scala.language.postfixOps + +//noinspection NonAsciiCharacters +class 喵呜 (using coeur: MornyCoeur) { + + object 抱抱 extends ISimpleCommand { + override val name: String = "抱抱" + override val aliases: Array[ICommandAlias]|Null = null + override def execute (using command: InputCommand, event: Update): Unit = + replyingSet("贴贴", "贴贴") + } + + object 揉揉 extends ISimpleCommand { + override val name: String = "揉揉" + override val aliases: Array[ICommandAlias]|Null = null + override def execute (using command: InputCommand, event: Update): Unit = + replyingSet("蹭蹭", "摸摸") + } + + object 蹭蹭 extends ISimpleCommand { + override val name: String = "蹭蹭" + override val aliases: Array[ICommandAlias]|Null = null + override def execute (using command: InputCommand, event: Update): Unit = + replyingSet("揉揉", "蹭蹭") + } + + object 贴贴 extends ISimpleCommand { + override val name: String = "贴贴" + override val aliases: Array[ICommandAlias]|Null = null + override def execute (using command: InputCommand, event: Update): Unit = + replyingSet("贴贴", "贴贴") + } + + object Progynova extends ITelegramCommand { + override val name: String = "install" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "" + override val description: String = "抽取一个神秘盒子" + override def execute (using command: InputCommand, event: Update): Unit = { + coeur.account exec new SendSticker( + event.message.chat.id, + TelegramStickers ID_PROGYNOVA + ).replyToMessageId(event.message.messageId) + } + } + + private def replyingSet (whileRec: String, whileNew: String)(using event: Update): Unit = { + val isNew = event.message.replyToMessage == null + val target = if (isNew) event.message else event.message.replyToMessage + coeur.account exec new SendMessage( + event.message.chat.id, + if (isNew) whileNew else whileRec + ).replyToMessageId(target.messageId).parseMode(ParseMode HTML) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala new file mode 100644 index 0000000..0980f68 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala @@ -0,0 +1,29 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.UseMath.over +import cc.sukazyo.cono.morny.util.UseRandom.* +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendMessage + +//noinspection NonAsciiCharacters +class 私わね (using coeur: MornyCoeur) extends ISimpleCommand { + + override val name: String = "me" + override val aliases: Array[ICommandAlias] | Null = null + + override def execute (using command: InputCommand, event: Update): Unit = { + + if ((1 over 521) chance_is true) { + val text = "/打假" + coeur.account exec new SendMessage( + event.message.chat.id, + text + ).replyToMessageId(event.message.messageId) + } + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala new file mode 100644 index 0000000..ea3149b --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala @@ -0,0 +1,21 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListenerManager +import cc.sukazyo.cono.morny.MornyCoeur + +class MornyEventListeners (using manager: EventListenerManager) (using coeur: MornyCoeur) { + + manager.register( + // ACTIVITY_RECORDER + // KUOHUANHUAN_NEED_SLEEP + OnUniMeowTrigger(using coeur.commands), + OnUserRandom(), + OnQuestionMarkReply(), + OnUserSlashAction(), + OnCallMe(), + OnCallMsgSend(), + OnMedicationNotifyApply(), + OnEventHackHandle() + ) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala new file mode 100644 index 0000000..38b8f95 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala @@ -0,0 +1,39 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.bot.query.{InlineQueryUnit, MornyQueries} +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.InlineQueryResult +import com.pengrad.telegrambot.request.AnswerInlineQuery + +import scala.collection.mutable.ListBuffer +import scala.language.postfixOps +import scala.reflect.ClassTag + +class MornyOnInlineQuery (using queryManager: MornyQueries) (using coeur: MornyCoeur) extends EventListener { + + override def onInlineQuery (using update: Update): Boolean = { + + val results: List[InlineQueryUnit[_]] = queryManager query update + + var cacheTime = Int.MaxValue + var isPersonal = InlineQueryUnit.defaults.IS_PERSONAL + val resultAnswers = ListBuffer[InlineQueryResult[_]]() + for (r <- results) { + if (cacheTime > r.cacheTime) cacheTime = r.cacheTime + if (r isPersonal) isPersonal = true + resultAnswers += r.result + } + + if (results isEmpty) return false + + coeur.account exec AnswerInlineQuery( + update.inlineQuery.id, resultAnswers toArray:_* + ).cacheTime(cacheTime).isPersonal(isPersonal) + true + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnTelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnTelegramCommand.scala new file mode 100644 index 0000000..541070b --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnTelegramCommand.scala @@ -0,0 +1,34 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.command.MornyCommands +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.{Message, Update} + +class MornyOnTelegramCommand (using commandManager: MornyCommands) (using coeur: MornyCoeur) extends EventListener { + + override def onMessage (using update: Update): Boolean = { + + def _isCommandMessage(message: Message): Boolean = + if message.text eq null then false + else if !(message.text startsWith "/") then false + else if message.text startsWith "/ " then false + else true + + if !_isCommandMessage(update.message) then return false + val inputCommand = InputCommand(update.message.text drop 1) + if (!(inputCommand.command matches "^\\w+$")) + logger debug "not command" + false + else if ((inputCommand.target ne null) && (inputCommand.target != coeur.username)) + logger debug "not morny command" + false + else + logger debug "is command" + commandManager.execute(using inputCommand) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnUpdateTimestampOffsetLock.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnUpdateTimestampOffsetLock.scala new file mode 100644 index 0000000..89f09cd --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnUpdateTimestampOffsetLock.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.MornyCoeur +import com.pengrad.telegrambot.model.Update + +class MornyOnUpdateTimestampOffsetLock (using coeur: MornyCoeur) extends EventListener { + + private def isOutdated (timestamp: Int): Boolean = + timestamp < (coeur.config.eventOutdatedTimestamp/1000) + + override def onMessage (using update: Update): Boolean = isOutdated(update.message.date) + override def onEditedMessage (using update: Update): Boolean = isOutdated(update.editedMessage.date) + override def onChannelPost (using update: Update): Boolean = isOutdated(update.channelPost.date) + override def onEditedChannelPost (using update: Update): Boolean = isOutdated(update.editedChannelPost.date) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala new file mode 100644 index 0000000..244ee66 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -0,0 +1,104 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.{Chat, Message, Update, User} +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{ForwardMessage, GetChat, SendMessage, SendSticker} + +import scala.language.postfixOps + +class OnCallMe (using coeur: MornyCoeur) extends EventListener { + + private val me = coeur.config.trustedMaster + + override def onMessage (using update: Update): Boolean = { + + if update.message.text == null then return false + if update.message.chat.`type` != (Chat.Type Private) then return false + + //noinspection ScalaUnnecessaryParentheses + val success = if me == -1 then false else + (update.message.text toLowerCase) match + case "steam" | "sbeam" | "sdeam" => + requestItem(update.message.from, "STEAM LIBRARY") + case "hana paresu" | "花宫" | "内群" => + requestItem(update.message.from, "Hana Paresu") + case "dinner" | "lunch" | "breakfast" | "meal" | "eating" | "安妮今天吃什么" => + requestLastDinner(update.message) + case cc if cc startsWith "cc::" => + requestCustom(update.message) + case _ => + return false + + if success then + coeur.account exec SendSticker( + update.message.chat.id, + TelegramStickers ID_SENT + ).replyToMessageId(update.message.messageId) + else + coeur.account exec SendSticker( + update.message.chat.id, + TelegramStickers ID_501 + ).replyToMessageId(update.message.messageId) + + true + + } + + private def requestItem (user: User, itemHTML: String, extra: String|Null = null): Boolean = + coeur.account exec SendMessage( + me, + s"""request $itemHTML + |from ${user.fullnameRefHTML}${if extra == null then "" else "\n"+extra}""" + .stripMargin + ).parseMode(ParseMode HTML) + true + + private def requestLastDinner (req: Message): Boolean = { + if coeur.config.dinnerChatId == -1 then return false + var isAllowed = false + var lastDinnerData: Message|Null = null + if (coeur.trusted isTrusted_dinnerReader req.from.id) { + // todo: have issues + // i dont want to test it anymore... it might be deprecated soon + lastDinnerData = (coeur.account exec GetChat(coeur.config.dinnerChatId)).chat.pinnedMessage + val sendResp = coeur.account exec ForwardMessage( + req.from.id, + lastDinnerData.forwardFromChat.id, + lastDinnerData.forwardFromMessageId + ) + import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + def lastDinner_dateMillis: Long = lastDinnerData.forwardDate longValue; + coeur.account exec SendMessage( + req.from.id, + "on %s [UTC+8]\n- %s before".formatted( + h(formatDate(lastDinner_dateMillis, 8)), + h(formatDuration(lastDinner_dateMillis)) + ) + ).parseMode(ParseMode HTML).replyToMessageId(sendResp.message.messageId) + isAllowed = true + } else { + coeur.account exec SendSticker( + req.from.id, + TelegramStickers ID_403 + ).replyToMessageId(req.messageId) + } + import Math.abs + requestItem( + req.from, "Last Annie Dinner", + if isAllowed then s"Allowed and returned https://t.me/c/${abs(lastDinnerData.forwardFromChat.id+1000000000000L)}/${lastDinnerData.forwardFromMessageId}" + else "Forbidden by perm check." + ) + } + + private def requestCustom (message: Message): Boolean = + requestItem(message.from, "[???]") + coeur.account exec ForwardMessage(me, message.chat.id, message.messageId) + true + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala new file mode 100644 index 0000000..db920c0 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala @@ -0,0 +1,154 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.{Chat, Message, MessageEntity, Update} +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{GetChat, SendMessage, SendSticker} + +import scala.collection.mutable.ArrayBuffer +import scala.language.postfixOps +import scala.util.matching.Regex + +class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { + + private val REGEX_MSG_SENDREQ_DATA_HEAD: Regex = "^\\*msg(-?\\d+)(\\*\\S+)?(?:\\n([\\s\\S]+))?$"r + + case class MessageToSend ( + message: String|Null, + entities: Array[MessageEntity]|Null, + parseMode: ParseMode|Null, + targetId: Long + ) { + def toSendMessage (target_override: Long|Null = null): SendMessage = + val useTarget = if target_override == null then targetId else target_override + val sendMessage = SendMessage(useTarget, message) + if entities ne null then sendMessage.entities(entities:_*) + if parseMode ne null then sendMessage.parseMode(parseMode) + sendMessage + } + private object MessageToSend: + def from (raw: Message): MessageToSend = { + raw.text match + case REGEX_MSG_SENDREQ_DATA_HEAD(_target, _parseMode, _body) => + val target = _target toLong + val parseMode: ParseMode | Null = _parseMode match + case "*markdown" | "*md" | "*m↓" => ParseMode MarkdownV2 + case "*md1" => ParseMode Markdown + case "*html" => ParseMode HTML + case _ => null + val bodyOffset = "*msg".length + _target.length + (if _parseMode eq null then 0 else _parseMode.length) + 1 + val entities = ArrayBuffer.empty[MessageEntity] + if (raw.entities ne null) for (e <- raw.entities) + val _parsed = MessageEntity(e.`type`, e.offset - bodyOffset, e.length) + if e.url ne null then _parsed.url(e.url) + if e.user ne null then _parsed.user(e.user) + if e.language ne null then _parsed.language(e.language) + if e.customEmojiId ne null then _parsed.language(e.language) + entities += _parsed + MessageToSend(_body, entities toArray, parseMode, target) + case _ => null + } + + override def onMessage (using update: Update): Boolean = { + + val message = update.message + + if message.chat.`type` != Chat.Type.Private then return false + if message.text eq null then return false + if !(message.text startsWith "*msg") then return false + + if (!(coeur.trusted isTrusted message.from.id)) + coeur.account exec SendSticker( + message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(message.messageId) + return true + + if (message.text == "*msgsend") { + + if (message.replyToMessage eq null) return answer404 + val messageToSend = MessageToSend from message.replyToMessage + if ((messageToSend eq null) || (messageToSend.message eq null)) return answer404 + val sendResponse = coeur.account execute messageToSend.toSendMessage() + + if (sendResponse isOk) { + coeur.account exec SendSticker( + update.message.chat.id, + TelegramStickers ID_SENT + ).replyToMessageId(update.message.messageId) + } else { + coeur.account exec SendMessage( + update.message.chat.id, + // language=html + s"""${sendResponse.errorCode} FAILED + |${sendResponse.description}""" + .stripMargin + ).replyToMessageId(update.message.messageId).parseMode(ParseMode HTML) + } + + return true + + } + + val messageToSend: MessageToSend = + val raw: Message = + if (message.text == "*msg") + if message.replyToMessage eq null then return answer404 + else message.replyToMessage + else if (message.text startsWith "*msg") + message + else return answer404 + val _toSend = MessageToSend from raw + if _toSend eq null then return answer404 + else _toSend + + val targetChatResponse = coeur.account execute GetChat(messageToSend.targetId) + if (targetChatResponse isOk) { + def getChatDescriptionHTML (chat: Chat): String = + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + // language=html + s"""${h(chat.id toString)}@${h(chat.`type`.name)}${if (chat.`type` != Chat.Type.Private) ":::" else ""} + |${chat.typeTag} ${h(chat.safe_name)} ${chat.safe_linkHTML}""" + .stripMargin + coeur.account exec SendMessage( + update.message.chat.id, + getChatDescriptionHTML(targetChatResponse.chat) + ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) + } else { + coeur.account exec SendMessage( + update.message.chat.id, + // language=html + s"""${targetChatResponse.errorCode} FAILED + |${targetChatResponse.description}""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) + } + + if messageToSend.message eq null then return true + val testSendResponse = coeur.account execute + messageToSend.toSendMessage(update.message.chat.id).replyToMessageId(update.message.messageId) + if (!(testSendResponse isOk)) + coeur.account exec SendMessage( + update.message.chat.id, + // language=html + s"""${testSendResponse.errorCode} FAILED + |${testSendResponse.description}""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) + + true + + } + + private def answer404 (using update: Update): Boolean = + coeur.account exec SendSticker( + update.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(update.message.messageId) + true + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala new file mode 100644 index 0000000..d6bc1f6 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala @@ -0,0 +1,47 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import com.google.gson.GsonBuilder +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.collection.mutable +import scala.language.postfixOps + +class OnEventHackHandle (using coeur: MornyCoeur) extends EventListener { + + import coeur.daemons.eventHack.trigger + + override def onMessage (using update: Update): Boolean = + trigger(update.message.chat.id, update.message.from.id) + override def onEditedMessage (using update: Update): Boolean = + trigger(update.editedMessage.chat.id, update.editedMessage.from.id) + override def onChannelPost (using update: Update): Boolean = + trigger(update.channelPost.chat.id, 0) + override def onEditedChannelPost (using update: Update): Boolean = + trigger(update.editedChannelPost.chat.id, 0) + override def onInlineQuery (using update: Update): Boolean = + trigger(0, update.inlineQuery.from.id) + override def onChosenInlineResult (using update: Update): Boolean = + trigger(0, update.chosenInlineResult.from.id) + override def onCallbackQuery (using update: Update): Boolean = + trigger(0, update.callbackQuery.from.id) + override def onShippingQuery (using update: Update): Boolean = + trigger(0, update.shippingQuery.from.id) + override def onPreCheckoutQuery (using update: Update): Boolean = + trigger(0, update.preCheckoutQuery.from.id) + override def onPoll (using update: Update): Boolean = + trigger(0, 0) + override def onPollAnswer (using update: Update): Boolean = + trigger(0, update.pollAnswer.user.id) + override def onMyChatMemberUpdated (using update: Update): Boolean = + trigger(update.myChatMember.chat.id, update.myChatMember.from.id) + override def onChatMemberUpdated (using update: Update): Boolean = + trigger(update.chatMember.chat.id, update.chatMember.from.id) + override def onChatJoinRequest (using update: Update): Boolean = + trigger(update.chatJoinRequest.chat.id, update.chatJoinRequest.from.id) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala new file mode 100644 index 0000000..25b29c1 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala @@ -0,0 +1,21 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.daemon.{MedicationTimer, MornyDaemons} +import com.pengrad.telegrambot.model.{Message, Update} + +class OnMedicationNotifyApply (using coeur: MornyCoeur) extends EventListener { + + override def onEditedMessage (using event: Update): Boolean = + editedMessageProcess(event.editedMessage) + override def onEditedChannelPost (using event: Update): Boolean = + editedMessageProcess(event.editedChannelPost) + + private def editedMessageProcess (edited: Message): Boolean = { + if edited.chat.id != coeur.config.medicationNotifyToChat then return false + coeur.daemons.medicationTimer.refreshNotificationWrite(edited) + true + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala new file mode 100644 index 0000000..1aff5f2 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -0,0 +1,44 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.event.OnQuestionMarkReply.isAllMessageMark +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps +import scala.util.boundary + +class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener { + + override def onMessage (using event: Update): Boolean = { + + if event.message.text eq null then return false + + import cc.sukazyo.cono.morny.util.UseMath.over + import cc.sukazyo.cono.morny.util.UseRandom.chance_is + if (1 over 8) chance_is false then return false + if !isAllMessageMark(using event.message.text) then return false + + coeur.account exec SendMessage( + event.message.chat.id, event.message.text + ).replyToMessageId(event.message.messageId) + true + + } + +} + +object OnQuestionMarkReply { + + private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') + + def isAllMessageMark (using text: String): Boolean = { + boundary[Boolean] { + for (c <- text) if QUESTION_MARKS contains c then boundary.break(false) + true + } + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala new file mode 100644 index 0000000..d1c97da --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala @@ -0,0 +1,24 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.bot.command.MornyCommands +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.MornyCoeur +import com.pengrad.telegrambot.model.Update + +class OnUniMeowTrigger (using commands: MornyCommands) (using coeur: MornyCoeur) extends EventListener { + + override def onMessage (using update: Update): Boolean = { + + if update.message.text eq null then return false + var ok = false + for ((name, command) <- commands.commands_uni) + val _name = "/"+name + if (_name == update.message.text) + command.execute(using InputCommand(_name)) + ok = true + ok + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala new file mode 100644 index 0000000..0611122 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala @@ -0,0 +1,41 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps + +class OnUserRandom (using coeur: MornyCoeur) extends EventListener { + + private val USER_OR_QUERY = "^(.+)(?:还是|or)(.+)$"r + private val USER_IF_QUERY = "^(.+)(?:吗\\?|?|\\?|吗?)$"r + + override def onMessage(using update: Update): Boolean = { + + if update.message.text == null then return false + if !(update.message.text startsWith "/") then return false + + import cc.sukazyo.cono.morny.util.UseRandom.rand_half + val query = update.message.text substring 1 + val result: String|Null = query match + case USER_OR_QUERY(_con1, _con2) => + if rand_half then _con1 else _con2 + case USER_IF_QUERY(_con) => + // for capability with [[OnQuestionMarkReply]] + if OnQuestionMarkReply.isAllMessageMark(using _con) then return false + (if rand_half then "不" else "") + _con + case _ => null + + if result == null then return false + + coeur.account exec SendMessage( + update.message.chat.id, result + ).replyToMessageId(update.message.messageId) + true + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala new file mode 100644 index 0000000..eb3e119 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala @@ -0,0 +1,79 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.UniversalCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps + +class OnUserSlashAction (using coeur: MornyCoeur) extends EventListener { + + private val TG_FORMAT = "^\\w+(@\\w+)?$"r + + override def onMessage (using update: Update): Boolean = { + + val text = update.message.text + if text == null then return false + + if (text startsWith "/") { + + // there has to be some special conditions for DP7 + // due to I have left DP7, I closed those special + // conditions. + // that is 2022, May 28th + // when one year goes, These code have rewrite with + // scala, those commented code is removed permanently. + // these message, here to remember the old DP7. + + val actions = UniversalCommand.Lossy(text) + actions(0) = actions(0) substring 1 + + actions(0) + + actions(0) match + // ignore Telegram command like + case TG_FORMAT(_) => + return false + // ignore Path link + case x if x contains "/" => return false + case _ => + + val isHardParse = actions(0) isBlank + def hp_len(i: Int) = if isHardParse then i+1 else i + if isHardParse && actions.length < 2 then return false + val v_verb = actions(hp_len(0)) + val hasObject = actions.length != hp_len(1) + val v_object = + if hasObject then + actions slice(hp_len(1), actions.length) mkString " " + else "" + val origin = update.message + val target = + if update.message.replyToMessage == null then + origin + else update.message.replyToMessage + + coeur.account exec SendMessage( + update.message.chat.id, + "%s %s%s %s %s!".format( + origin.sender_firstnameRefHTML, + h(v_verb), if hasObject then "" else "了", + if (origin == target) + s"自己" + else target.sender_firstnameRefHTML, + if hasObject then h(v_object+" ") else "" + ) + ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) + true + + } else false + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala new file mode 100644 index 0000000..a7a2b99 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala @@ -0,0 +1,12 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.MornyCoeur +import com.pengrad.telegrambot.model.Update + +import javax.annotation.Nullable + +trait ITelegramQuery { + + def query (event: Update): List[InlineQueryUnit[_]] | Null + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/InlineQueryUnit.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/InlineQueryUnit.scala new file mode 100644 index 0000000..b15f4b6 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/InlineQueryUnit.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.bot.query.InlineQueryUnit.defaults +import com.pengrad.telegrambot.model.request.InlineQueryResult + +object InlineQueryUnit { + + object defaults: + val CACHE_TIME = 300 + val IS_PERSONAL = false + +} + +class InlineQueryUnit[T <: InlineQueryResult[T]](val result: T) { + + var cacheTime: Int = defaults.CACHE_TIME + var isPersonal: Boolean = defaults.IS_PERSONAL + + def cacheTime (v: Int): InlineQueryUnit[T] = + this.cacheTime = v + this + + def isPersonal (v: Boolean): InlineQueryUnit[T] = + this.isPersonal = v + this + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala new file mode 100644 index 0000000..11c6a12 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.bot.query +import cc.sukazyo.cono.morny.MornyCoeur +import com.pengrad.telegrambot.model.Update + +import scala.collection.mutable.ListBuffer + +class MornyQueries (using MornyCoeur) { + + private val queryInstances = Set[ITelegramQuery]( + RawText(), + MyInformation(), + ShareToolTwitter(), + ShareToolBilibili() + ) + + def query (event: Update): List[InlineQueryUnit[_]] = { + val results = ListBuffer[InlineQueryUnit[_]]() + for (instance <- queryInstances) { + val r = instance query event + if (r != null) results ++= r + } + results.result() + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala new file mode 100644 index 0000000..1b27068 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala @@ -0,0 +1,30 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} + +import scala.language.postfixOps + +class MyInformation extends ITelegramQuery { + + private val ID_PREFIX = "[morny/info/me]" + private val TITLE = "My Account Information" + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + if !((event.inlineQuery.query eq null) || (event.inlineQuery.query isEmpty)) then return null + + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(ID_PREFIX), TITLE, + new InputTextMessageContent( + TelegramUserInformation getFormattedInformation event.inlineQuery.from + ).parseMode(ParseMode HTML) + )).isPersonal(true).cacheTime(10) + ) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala new file mode 100644 index 0000000..036fb1d --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala @@ -0,0 +1,26 @@ +package cc.sukazyo.cono.morny.bot.query +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent} + +import scala.language.postfixOps + +class RawText extends ITelegramQuery { + + private val ID_PREFIX = "[morny/r/text]" + private val TITLE = "Raw Text" + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + if (event.inlineQuery.query == null || (event.inlineQuery.query isBlank)) return null + + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(ID_PREFIX, event.inlineQuery.query), TITLE, + InputTextMessageContent(event.inlineQuery.query) + )) + ) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala new file mode 100644 index 0000000..f73fed6 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -0,0 +1,77 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId +import cc.sukazyo.cono.morny.util.BiliTool +import cc.sukazyo.cono.morny.util.UseSelect.select +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} + +import scala.language.postfixOps +import scala.util.matching.Regex + +class ShareToolBilibili extends ITelegramQuery { + + private val TITLE_BILI_AV = "[bilibili] Share video / av" + private val TITLE_BILI_BV = "[bilibili] Share video / BV" + private val ID_PREFIX_BILI_AV = "[morny/share/bili/av]" + private val ID_PREFIX_BILI_BV = "[morny/share/bili/bv]" + private val LINK_PREFIX = "https://bilibili.com/video/" + private val REGEX_BILI_VIDEO: Regex = "^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$"r + private val SHARE_FORMAT_HTML = "%s" + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + if (event.inlineQuery.query == null) return null + + event.inlineQuery.query match + case REGEX_BILI_VIDEO(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) => + + logger debug + s"""====== Share Tool Bilibili Catch ok + |1: ${_url_v} + |2: ${_url_av} + |3: ${_url_bv} + |4: ${_url_param} + |5: ${_url_v_part} + |6: ${_raw_av} + |7: ${_raw_bv}""" + .stripMargin + + var av = select(_url_av, _raw_av) + var bv = select(_url_bv, _raw_bv) + logger trace s"catch id av[$av] bv[$bv]" + val part: Int|Null = if (_url_v_part!=null) _url_v_part toInt else null + logger trace s"catch video part[$part]" + + if (av == null) { + assert (bv != null) + av = BiliTool.toAv(bv) toString; + logger trace s"converted bv[$av] to av[$av]" + } else { + bv = BiliTool.toBv(av toLong) + logger trace s"converted av[$av] to bv[$bv]" + } + + val id_av = s"av$av" + val id_bv = s"BV$bv" + val linkParams = if (part!=null) s"?p=$part" else "" + val link_av = LINK_PREFIX + id_av + linkParams + val link_bv = LINK_PREFIX + id_bv + linkParams + + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av, + InputTextMessageContent(SHARE_FORMAT_HTML.format(link_av, id_av)).parseMode(ParseMode HTML) + )), + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(ID_PREFIX_BILI_BV + bv), TITLE_BILI_BV + bv, + InputTextMessageContent(SHARE_FORMAT_HTML.format(link_bv, id_bv)).parseMode(ParseMode HTML) + )) + ) + + case _ => null + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala new file mode 100644 index 0000000..a550501 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -0,0 +1,40 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.InlineQueryResultArticle + +import scala.language.postfixOps +import scala.util.matching.Regex + +class ShareToolTwitter extends ITelegramQuery { + + private val TITLE_VX = "[tweet] Share as VxTwitter" + private val TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)" + private val ID_PREFIX_VX = "[morny/share/twitter/vxtwi]" + private val ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]" + private val REGEX_TWEET_LINK: Regex = "^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$"r + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + if (event.inlineQuery.query == null) return null + + event.inlineQuery.query match + + case REGEX_TWEET_LINK(_, _path_data, _, _, _, _) => + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, + s"https://vxtwitter.com/$_path_data" + )), + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED, + s"https://c.vxtwitter.com/$_path_data" + )) + ) + + case _ => null + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala new file mode 100644 index 0000000..ee1ab67 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala @@ -0,0 +1,50 @@ +package cc.sukazyo.cono.morny.daemon + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.google.gson.GsonBuilder +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.collection.mutable + +class EventHacker (using coeur: MornyCoeur) { + + private case class Hacker (from_chat: Long, from_message: Long): + override def toString: String = s"$from_chat/$from_message" + + enum HackType: + case USER + case GROUP + case ANY + + private val hackers = mutable.HashMap.empty[String, Hacker] + + def registerHack (from_message: Long, from_user: Long, from_chat: Long, t: HackType): Unit = + val record = t match + case HackType.USER => s"(($from_user))" + case HackType.GROUP => s"{{$from_chat}}" + case HackType.ANY => "[[]]" + hackers += (record -> Hacker(from_chat, from_message)) + logger debug s"add hacker track $record" + + def trigger (chat: Long, fromUser: Long)(using update: Update): Boolean = { + logger debug s"got event signed {{$chat}}(($fromUser))" + val x: Hacker = + if hackers contains s"(($fromUser))" then (hackers remove s"(($fromUser))") get + else if hackers contains s"{{$chat}}" then (hackers remove s"{{$chat}}") get + else if hackers contains "[[]]" then (hackers remove "[[]]") get + else return false + logger debug s"hacked event by $x" + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + coeur.account exec SendMessage( + x.from_chat, + // language=html + s"${h(GsonBuilder().setPrettyPrinting().create.toJson(update))}" + ).parseMode(ParseMode HTML).replyToMessageId(x.from_message toInt) + true + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala new file mode 100644 index 0000000..3c61872 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -0,0 +1,103 @@ +package cc.sukazyo.cono.morny.daemon + +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.daemon.MedicationTimer.calcNextRoutineTimestamp +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.pengrad.telegrambot.model.{Message, MessageEntity} +import com.pengrad.telegrambot.request.{EditMessageText, SendMessage} +import com.pengrad.telegrambot.response.SendResponse + +import java.time.{LocalDateTime, ZoneOffset} +import scala.collection.mutable.ArrayBuffer +import scala.language.implicitConversions + +class MedicationTimer (using coeur: MornyCoeur) extends Thread { + + private val NOTIFY_MESSAGE = "🍥⏲" + private val DAEMON_THREAD_NAME_DEF = "MedicationTimer" + + private val use_timeZone = coeur.config.medicationTimerUseTimezone + import scala.jdk.CollectionConverters.SetHasAsScala + private val notify_atHour: Set[Int] = coeur.config.medicationNotifyAt.asScala.toSet.map(_.intValue) + private val notify_toChat = coeur.config.medicationNotifyToChat + + this.setName(DAEMON_THREAD_NAME_DEF) + + private var lastNotify_messageId: Option[Int] = None + + override def run (): Unit = { + + if ((notify_toChat == -1) || (notify_atHour isEmpty)) { + logger info "Medication Timer disabled : related param is not complete set" + return + } + + logger info "Medication Timer started." + while (!this.isInterrupted) { + try { + waitToNextRoutine() + sendNotification() + } catch + case _: InterruptedException => + interrupt() + logger info "MedicationTimer was interrupted, will be exit now" + case ill: IllegalArgumentException => + logger warn "MedicationTimer will not work due to: " + ill.getMessage + interrupt() + case e => + logger error + s"""unexpected error occurred on NotificationTimer + |${exceptionLog(e)}""" + .stripMargin + coeur.daemons.reporter.exception(e) + } + logger info "Medication Timer stopped." + + } + + private def sendNotification(): Unit = { + val sendResponse: SendResponse = coeur.account exec SendMessage(notify_toChat, NOTIFY_MESSAGE) + if sendResponse isOk then lastNotify_messageId = Some(sendResponse.message.messageId) + else lastNotify_messageId = None + } + + @throws[InterruptedException | IllegalArgumentException] + private def waitToNextRoutine (): Unit = { + Thread sleep calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour) + } + + def refreshNotificationWrite (edited: Message): Unit = { + if (lastNotify_messageId isEmpty) || (lastNotify_messageId.get != (edited.messageId toInt)) then return + import cc.sukazyo.cono.morny.util.CommonFormat.formatDate + val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60) + val entities = ArrayBuffer.empty[MessageEntity] + if edited.entities ne null then entities ++= edited.entities + entities += MessageEntity(MessageEntity.Type.italic, edited.text.length + "\n-- ".length, editTime.length) + coeur.account exec EditMessageText( + notify_toChat, + edited.messageId, + edited.text + s"\n-- $editTime --" + ).entities(entities toArray:_*) + lastNotify_messageId = None + } + +} + +object MedicationTimer { + + @throws[IllegalArgumentException] + private[daemon] def calcNextRoutineTimestamp (baseTimeMillis: Long, zone: ZoneOffset, notifyAt: Set[Int]): Long = { + if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set") + var time = LocalDateTime.ofEpochSecond( + baseTimeMillis / 1000, ((baseTimeMillis % 1000) * 1000 * 1000) toInt, + zone + ).withMinute(0).withSecond(0).withNano(0) + time = time plusHours 1 + while (!(notifyAt contains(time getHour))) { + time = time plusHours 1 + } + (time toInstant zone) toEpochMilli + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala new file mode 100644 index 0000000..96ae18c --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala @@ -0,0 +1,31 @@ +package cc.sukazyo.cono.morny.daemon + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur + +class MornyDaemons (using val coeur: MornyCoeur) { + + val medicationTimer: MedicationTimer = MedicationTimer() + val reporter: MornyReport = MornyReport() + val eventHack: EventHacker = EventHacker() + + def start (): Unit = { + logger info "ALL Morny Daemons starting..." + // TrackerDataManager.init(); + medicationTimer.start() + logger info "Morny Daemons started." + + } + + def stop (): Unit = { + logger.info("stopping All Morny Daemons...") + // TrackerDataManager.DAEMON.interrupt(); + medicationTimer.interrupt() + // TrackerDataManager.trackingLock.lock(); + try { medicationTimer.join() } + catch case e: InterruptedException => + e.printStackTrace(System.out) + logger.info("stopped ALL Morny Daemons.") + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala new file mode 100644 index 0000000..00ff369 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -0,0 +1,123 @@ +package cc.sukazyo.cono.morny.daemon + +import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig} +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.data.MornyInformation.getVersionAllFullTagHTML +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import com.google.gson.GsonBuilder +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.model.User +import com.pengrad.telegrambot.request.{BaseRequest, SendMessage} +import com.pengrad.telegrambot.response.BaseResponse + +class MornyReport (using coeur: MornyCoeur) { + + private val enabled = coeur.config.reportToChat != -1 + if !enabled then + logger info "Morny Report is disabled : report chat is set to -1" + + private def executeReport[T <: BaseRequest[T, R], R<: BaseResponse] (report: T): Unit = { + if !enabled then return; + try { + coeur.account exec report + } catch case e: EventRuntimeException.ActionFailed => { + logger warn + s"""cannot execute report to telegram: + |${exceptionLog(e) indent 4} + | tg-api response: + |${(e.response toString) indent 4}""" + .stripMargin + } + } + + def exception (e: Throwable, description: String|Null = null): Unit = { + def _tgErrFormat: String = e match + case api: EventRuntimeException.ActionFailed => + // language=html + "\n\ntg-api error:\n
%s
" + .formatted(GsonBuilder().setPrettyPrinting().create.toJson(api.response)) + case _ => "" + executeReport(SendMessage( + coeur.config.reportToChat, + // language=html + s"""▌Coeur Unexpected Exception + |${if description ne null then h(description)+"\n" else ""} + |
${h(exceptionLog(e))}
$_tgErrFormat""" + .stripMargin + ).parseMode(ParseMode HTML)) + } + + def unauthenticatedAction (action: String, user: User): Unit = { + executeReport(SendMessage( + coeur.config.reportToChat, + // language=html + s"""▌User unauthenticated action + |action: ${h(action)} + |by user ${user.fullnameRefHTML}""" + .stripMargin + ).parseMode(ParseMode HTML)) + } + + def reportCoeurMornyLogin(): Unit = { + executeReport(SendMessage( + coeur.config.reportToChat, + // language=html + s"""▌Morny Logged in + |-v $getVersionAllFullTagHTML + |as user ${coeur.username} + | + |as config fields: + |${sectionConfigFields(coeur.config)}""" + .stripMargin + ).parseMode(ParseMode HTML)) + } + + private def sectionConfigFields (config: MornyConfig): String = { + val echo = StringBuilder() + for (field <- config.getClass.getFields) { + // language=html + echo ++= s"- ${field.getName} " + try { + if (field.isAnnotationPresent(classOf[MornyConfig.Sensitive])) { + echo ++= /*language=html*/ ": sensitive_field" + } else { + val value = field.get(config) + // language=html + echo ++= "= " ++= (if value eq null then "null" else s"${h(value.toString)}") + } + + } catch + // noinspection ScalaUnnecessaryParentheses + case e: (IllegalAccessException|IllegalArgumentException|NullPointerException) => + // language=html + echo ++= s": ${h("")}" + logger error + s"""error while reading config field ${field.getName} + |${exceptionLog(e)}""".stripMargin + exception(e, s"error while reading config field ${field.getName}") + echo ++= "\n" + } + echo dropRight 1 toString + } + + def reportCoeurExit (): Unit = { + def _causedTag = coeur.exitReason match + case Some(_exitReason) => _exitReason match + case u: User => u.fullnameRefHTML + case a => /*language=html*/ s"${h(a.toString)}" + case None => "UNKNOWN reason" + executeReport(SendMessage( + coeur.config.reportToChat, + // language=html + s"""▌Morny Exited + |from user @${coeur.username} + | + |by: $_causedTag""" + .stripMargin + ).parseMode(ParseMode HTML)) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala new file mode 100644 index 0000000..19e4642 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala @@ -0,0 +1,45 @@ +package cc.sukazyo.cono.morny.data + +import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornySystem} + +import java.net.InetAddress +import java.rmi.UnknownHostException + +object MornyInformation { + + //noinspection ScalaWeakerAccess + def getVersionGitTagHTML: String = { + if (!MornySystem.isGitBuild) return "" + val g = StringBuilder() + val cm = BuildConfig.COMMIT substring(0, 8) + val cp = MornySystem.currentCodePath + if (cp == null) g ++= s"$cm" + else g ++= s"$cm" + if (!MornySystem.isCleanBuild) g ++= ".δ" + g toString + } + + def getVersionAllFullTagHTML: String = { + val v = StringBuilder() + v ++= s"${MornySystem VERSION_BASE}" + if (MornySystem isUseDelta) v ++= s"-δ${MornySystem VERSION_DELTA}" + if (MornySystem isGitBuild) v ++= "+git." ++= getVersionGitTagHTML + v ++= s"*${MornySystem.CODENAME toUpperCase}" + v toString + } + + //noinspection ScalaWeakerAccess + def getRuntimeHostname: Option[String] = { + try Some(InetAddress.getLocalHost.getHostName) + catch case _: UnknownHostException => None + } + + def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get + + def getMornyAboutLinksHTML: String = + s"""source code | backup + |反馈 / issue tracker + |使用说明书 / user guide & docs""" + .stripMargin + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala new file mode 100644 index 0000000..b707576 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny.data + +import com.pengrad.telegrambot.model.User + +import scala.language.postfixOps + +object MornyJrrp { + + def jrrp_of_telegramUser (user: User, timestamp: Long): Double = + jrrp_v_xmomi(user.id, timestamp/(1000*60*60*24)) * 100.0 + + private def jrrp_v_xmomi (identifier: Long, dayStamp: Long): Double = + import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5 + import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + java.lang.Long.parseLong(MD5(s"$identifier@$dayStamp").toHex.substring(0, 4), 16) / (0xffff toDouble) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/NbnhhshQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/data/NbnhhshQuery.scala new file mode 100644 index 0000000..9c2049a --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/NbnhhshQuery.scala @@ -0,0 +1,37 @@ +package cc.sukazyo.cono.morny.data + +import cc.sukazyo.cono.morny.util.OkHttpPublic.MediaTypes +import com.google.gson.Gson +import okhttp3.{OkHttpClient, Request, RequestBody, ResponseBody} + +import java.io.IOException +import scala.util.Using + +object NbnhhshQuery { + + case class Word (name: String, trans: Array[String], inputting: Array[String]) + case class GuessResult (words: Array[Word]) + + private case class GuessRequest (text: String) + + private val API_URL = "https://lab.magiconch.com/api/nbnhhsh/" + private val API_GUESS_METHOD = "guess/" + + private val httpClient = OkHttpClient() + + @throws[IOException] + def sendGuess (text: String): GuessResult = { + val requestJsonText = Gson().toJson(GuessRequest(text)) + val request = Request.Builder() + .url(API_URL + API_GUESS_METHOD) + .post(RequestBody.create(requestJsonText, MediaTypes.JSON)) + .build + Using (httpClient.newCall(request).execute) { response => + val body = response.body + if body eq null then throw IOException("Nbnhhsh Request: body is null.") + val x = s"{ 'words': ${body.string} }" + Gson().fromJson(x, classOf[GuessResult]) + }.get + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala new file mode 100644 index 0000000..ac45039 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala @@ -0,0 +1,37 @@ +package cc.sukazyo.cono.morny.data + +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.MornyAssets +import cc.sukazyo.cono.morny.daemon.MornyReport +import cc.sukazyo.cono.morny.MornyAssets.AssetsException + +import java.io.IOException +import scala.language.postfixOps +import scala.util.Using + +object TelegramImages { + + class AssetsFileImage (assetsPath: String) { + + private var cache: Option[Array[Byte]] = None + + @throws[AssetsException] + def get:Array[Byte] = + if cache isEmpty then read() + cache.get + + @throws[AssetsException] + private def read (): Unit = { + Using ((MornyAssets.pack getResource assetsPath)read) { stream => + try { this.cache = Some(stream.readAllBytes()) } + catch case e: IOException => { + throw AssetsException(e) + } + } + } + + } + + val IMG_ABOUT: AssetsFileImage = AssetsFileImage("images/featured-image@0.5x.jpg") + +} diff --git a/src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java similarity index 53% rename from src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java rename to src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java index fb3f01c..51aa33f 100644 --- a/src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java @@ -1,11 +1,9 @@ package cc.sukazyo.cono.morny.data; -import cc.sukazyo.cono.morny.util.tgapi.ExtraAction; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; -import com.pengrad.telegrambot.response.SendResponse; - +import javax.annotation.Nonnull; import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Map; /** * 存放 bot 使用到的贴纸 @@ -23,42 +21,30 @@ public class TelegramStickers { public static final String ID_SAVED = "CAACAgEAAx0CSQh32gABBExuYdB_G0srfhQldRWkBYxWzCOv4-IAApooAAJ4_MYFcjuNZszfQcQjBA"; public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ"; public static final String ID_NETWORK_ERR = "CAACAgEAAxkBAAID0WNJgNEkD726KW4vZeFlw0FlVVyNAAIXJgACePzGBb50o7O1RbxoKgQ"; + public static final String ID_501 = "CAACAgEAAxkBAAIHbGUhJ8zm2Sb_c0YU-DYQ6xb-ZDtaAAKdJwACePzGBTOftDZL6X7vMAQ"; - public static void echoAllStickers (ExtraAction actionObject, long sentChat, int replyToMessageId) { - + @Nonnull + public static Map map () { + final LinkedHashMap mapping = new LinkedHashMap<>(); for (Field object : TelegramStickers.class.getFields()) { if (object.getType()==String.class && object.getName().startsWith("ID_")) { try { - - final String stickerId = (String)object.get(""); - SendSticker echo = new SendSticker(sentChat, stickerId); - SendMessage echoName = new SendMessage(sentChat, object.getName()); - if (replyToMessageId!=-1) echo.replyToMessageId(replyToMessageId); - SendResponse echoedName = actionObject.exec(echoName); - actionObject.exec(echo.replyToMessageId(echoedName.message().messageId())); - + mapping.put(object.getName(), (String)object.get("")); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } - + return mapping; } - public static void echoStickerByID (String stickerFieldID, ExtraAction actionObject, long sentChat, int replyToMessageId) { + @Nonnull + public static Map.Entry getById (@Nonnull String stickerFieldID) + throws NoSuchFieldException { try { // normally get the sticker and echo - Field sticker = TelegramStickers.class.getField(stickerFieldID); - SendMessage echoName = new SendMessage(sentChat, sticker.getName()); - SendSticker echo = new SendSticker(sentChat, (String)sticker.get("")); - if (replyToMessageId!=-1) echo.replyToMessageId(replyToMessageId); - SendResponse echoedName = actionObject.exec(echoName); - actionObject.exec(echo.replyToMessageId(echoedName.message().messageId())); - } catch (NoSuchFieldException e) { - // no such sticker found - SendSticker echo404 = new SendSticker(sentChat, TelegramStickers.ID_404); - if (replyToMessageId!=-1) echo404.replyToMessageId(replyToMessageId); - actionObject.exec(echo404); + Field field = TelegramStickers.class.getField(stickerFieldID); + return Map.entry(field.getName(), (String)field.get("")); } catch (IllegalAccessException e) { // java-reflect get sticker FILE_ID failed throw new RuntimeException(e); diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala new file mode 100644 index 0000000..d045cc5 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala @@ -0,0 +1,41 @@ +package cc.sukazyo.cono.morny.data.ip186 + +import okhttp3.{OkHttpClient, Request} + +import java.io.IOException +import scala.language.postfixOps +import scala.util.Using + +object IP186QueryHandler { + + private val SITE_URL = "https://ip.186526.xyz/" + private val QUERY_PARAM_IP = "type=json&format=true" + private val QUERY_PARAM_WHOIS = "type=plain" + + private val httpClient = OkHttpClient() + + @throws[IOException] + def query_ip (ip: String): IP186Response = + commonQuery(SITE_URL + ip, QUERY_PARAM_IP) + + @throws[IOException] + //noinspection ScalaWeakerAccess + def query_whois (domain: String): IP186Response = + commonQuery(SITE_URL+"whois/"+domain, QUERY_PARAM_WHOIS) + + @throws[IOException] + def query_whoisPretty (domain: String): IP186Response = + val raw = query_whois(domain) + IP186Response(raw.url, raw.body substring(0, (raw.body indexOf "<<<")+3)) + + @throws[IOException] + private def commonQuery (requestUrl: String, queryParam: String): IP186Response = { + val request = Request.Builder().url(requestUrl + "?" + queryParam).build + Using ((httpClient newCall request) execute) { response => + val _body = response.body + if _body eq null then throw IOException("Response of ip186: body is empty!") + IP186Response(requestUrl, _body.string) + }.get + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186Response.scala b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186Response.scala new file mode 100644 index 0000000..6fcad79 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186Response.scala @@ -0,0 +1,3 @@ +package cc.sukazyo.cono.morny.data.ip186 + +case class IP186Response (url: String, body: String) diff --git a/src/main/scala/cc/sukazyo/cono/morny/internal/BuildConfigField.java b/src/main/scala/cc/sukazyo/cono/morny/internal/BuildConfigField.java new file mode 100644 index 0000000..f9931ce --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/internal/BuildConfigField.java @@ -0,0 +1,14 @@ +package cc.sukazyo.cono.morny.internal; + + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * 这个注解表示当前字段是由 gradle 任务 {@code generateBuildConfig} 自动生成的. + * @since 1.0.0-alpha4 + */ +@Documented +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface BuildConfigField {} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala b/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala new file mode 100644 index 0000000..37a2473 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala @@ -0,0 +1,119 @@ +package cc.sukazyo.cono.morny.util + +import cc.sukazyo.cono.morny.util.UseMath.** + +import scala.collection.mutable + +/** Utils about $Bilibili + * + * contains utils: + * - av/BV converting: + * - [[toAv]] + * - [[toBv]] + * + * @define Bilibili [[https://bilibili.com Bilibili]] + * + * @define AvBvFormat + * === About AV/BV id format === + * the AV id is a number; the BV id is a special 10 digits base58 number, it shows as String + * in programming. + * + * e.g. while the link ''`https://www.bilibili.com/video/BV17x411w7KC/`'' shows + * the same with ''`https://www.bilibili.com/video/av170001/`'', the AV id + * is __`170001`__, the BV id is __`BV17x411w7KC`__. + * + * @define AvBvSeeAlso [[https://www.zhihu.com/question/381784377/answer/1099438784 mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」?]] + * @todo Maybe make a class `AV`/`BV` and implement the parse in the class + */ +object BiliTool { + + private val V_CONV_XOR = 177451812L + private val V_CONV_ADD = 8728348608L + + private val BV_TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF" + private val TABLE_INT = BV_TABLE.length + private val BV_TABLE_REVERSED = + val mapping = mutable.HashMap.empty[Char, Int] + for (i <- BV_TABLE.indices) mapping += (BV_TABLE(i) -> i) + mapping.toMap + private val BV_TEMPLATE = "1 4 1 7 " + private val BV_TEMPLATE_FILTER = Array(9, 8, 1, 6, 2, 4) + + /** Error of illegal BV id. + * + * @constructor Build a error with illegal BV details. + * @param bv the source illegal BV id. + * @param reason why it is illegal. + */ + class IllegalFormatException private (bv: String, reason: String) + extends RuntimeException (s"`$bv is not a valid 10 digits base58 BV id: $reason`") { + + /** Error of illegal BV id, where the reason is the BV id is not 10 digits. + * + * @param bv the source of illegal BV id. + * @param length the length of the illegal BV id. + */ + def this (bv: String, length: Int) = + this(bv, s"given length is $length") + + /** Error of illegal BV id, where the reason is the BV id contains non [[BV_TABLE base58 character]]. + * + * @param bv the source of illegal BV id. + * @param c the illegal character + * @param location the index of the illegal character in the illegal BV id. + */ + def this (bv: String, c: Char, location: Int) = + this(bv, s"char `$c` is not in base58 char table (in position $location)") + } + + /** Convert an AV video id format to BV video id format for $Bilibili + * + * $AvBvFormat + * + * this method '''available while the __av-id < 2^27^__''', while it theoretically + * available when the av-id < 2^30^. Meanwhile some digits of the BV id is a fixed + * value (like the [[BV_TEMPLATE]] shows) -- input __bv__ can do not follow the format, + * but it will almost certainly gives a wrong AV id (because the fixed number is not + * processed at all!) + * + * @see $AvBvSeeAlso + * + * @param bv a BV id, which should be exactly 10 digits and all chars should be + * a legal base58 char (which means can be found in [[BV_TABLE]]). + * otherwise, an [[IllegalFormatException]] will be thrown. + * @return an AV id which will shows the save video of input __bv__ in $Bilibili + * @throws IllegalFormatException when the input __bv__ is not a legal 10 digits base58 + * formatted BV id. + */ + @throws[IllegalFormatException] + def toAv (bv: String): Long = { + var av = 0L + if (bv.length != 10) throw IllegalFormatException(bv, bv.length) + for (i <- BV_TEMPLATE_FILTER.indices) { + val _get = BV_TEMPLATE_FILTER(i) + val tableToken = BV_TABLE_REVERSED get bv(_get) + if tableToken isEmpty then throw IllegalFormatException(bv, bv(_get), _get) + av = av + (tableToken.get * (TABLE_INT**i).toLong) + } + (av - V_CONV_ADD) ^ V_CONV_XOR + } + + /** Convert an AV video format to a BV video format for $Bilibili. + * + * this method '''available while the __av-id < 2^27^__''', while it theoretically + * available when the av-id < 2^30^. + * + * @param av an AV id. + * @return a BV id which will shows the save video of input __av__ in $Bilibili + */ + def toBv (av: Long): String = { + val _av = (av^V_CONV_XOR)+V_CONV_ADD + val bv = Array(BV_TEMPLATE:_*) + for (i <- BV_TEMPLATE_FILTER.indices) { + import Math.{floor, pow} + bv(BV_TEMPLATE_FILTER(i)) = BV_TABLE( (floor(_av/(TABLE_INT**i)) % TABLE_INT) toInt ) + } + String copyValueOf bv + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/CommonEncrypt.scala b/src/main/scala/cc/sukazyo/cono/morny/util/CommonEncrypt.scala new file mode 100644 index 0000000..740e048 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/CommonEncrypt.scala @@ -0,0 +1,98 @@ +package cc.sukazyo.cono.morny.util + +import java.nio.charset.{Charset, StandardCharsets} +import java.security.{MessageDigest, NoSuchAlgorithmException} + +/** Provides some re-encapsulated algorithm function, and some standard values in encrypting, + * and some normalized utils in processing something in encrypting. + * + * currently there's: + * - standard value: + * - [[ENCRYPT_STANDARD_CHARSET]] the standard [[Charset]] to parse between [[String]] + * and [[Bin]] in encrypting. + * - algorithm encapsulations: + * - [[MD5]] (MD5 Message-Digest Algorithm) + * - [[SHA1]] (Secure Hash Algorithm 1) + * - [[SHA256]] (Secure Hash Algorithm 2: 256bit) + * - [[SHA512]] (Secure Hash Algorithm 2: 512bit) + * - normalized utils + * - [[lint_base64FileName]] remove the .base64 file-extension for base64 text file + * + * @define WhenString2Bin + * [[String]] will encoded to [[Bin]] using [[Charset]] [[ENCRYPT_STANDARD_CHARSET]] + * + * @todo some tests + */ +object CommonEncrypt { + + /** the [[Charset]] should use when converting between [[String]] + * and [[Bin]] in encrypting */ + val ENCRYPT_STANDARD_CHARSET: Charset = StandardCharsets.UTF_8 + + /** the alias of [[Array]]`[`[[Byte]]`]`. + * means the binary data. + */ + //noinspection ScalaWeakerAccess + type Bin = Array[Byte] + + private def hash (data: Bin)(using algorithm: String): Bin = + try { + MessageDigest.getInstance(algorithm) digest data + } catch case n: NoSuchAlgorithmException => + throw IllegalStateException(n) + + /** the [[https://en.wikipedia.org/wiki/MD5 MD5]] hash value of input [[Bin]] `data`. */ + def MD5(data: Bin): Bin = hash(data)(using "md5") + /** the [[https://en.wikipedia.org/wiki/MD5 MD5]] hash value of input [[String]] `data`. + * + * $WhenString2Bin + */ + def MD5 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "md5") + + /** the [[https://en.wikipedia.org/wiki/SHA-1 SHA-1]] hash value of input [[Bin]] `data`. */ + def SHA1 (data: Bin): Bin = hash(data)(using "sha1") + /** the [[https://en.wikipedia.org/wiki/SHA-1 SHA-1]] hash value of input [[String]] `data`. + * + * $WhenString2Bin + */ + def SHA1 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha1") + + /** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/256]] hash value of input [[Bin]] `data`. */ + def SHA256 (data: Bin): Bin = hash(data)(using "sha256") + /** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/256]] hash value of input [[String]] `data`. + * + * $WhenString2Bin + */ + def SHA256 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha256") + + /** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/512]] hash value of input [[Bin]] `data`. */ + def SHA512 (data: Bin): Bin = hash(data)(using "sha512") + /** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/512]] hash value of input [[String]] `data`. + * + * $WhenString2Bin + */ + def SHA512 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha512") + + /** Try get the filename before it got encrypted. + * + * It assumes the base64 encrypted file should keep the original file, and plus + * a file-extension shows the file is base64 encrypted. + * + * Actually, the file will try find the following file-extension and drop it: + * - `.b64` + * - `.64.txt` + * - `.base64` + * - `.base64.txt` + * if none of those found, it will do no process anymore. + * + * @param encrypted the file fullname (means filename with file-extension) of base64 encrypted file. + * @return the file fullname removed the base64 file extension. + */ + def lint_base64FileName (encrypted: String): String = encrypted match + case i if i endsWith ".b64" => i dropRight ".b64".length + case ix if ix endsWith ".b64.txt" => ix dropRight ".b64.txt".length + case l if l endsWith ".base64" => l dropRight ".base64".length + case lx if lx endsWith ".base64.txt" => lx dropRight ".base64.txt".length + case u => u + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala b/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala new file mode 100644 index 0000000..0ab5d36 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala @@ -0,0 +1,76 @@ +package cc.sukazyo.cono.morny.util + +import java.time.{Instant, LocalDateTime, ZoneId, ZoneOffset} +import java.time.format.DateTimeFormatter + +/** Some formatting (convert some data to some standard output type) + * methods normalized based on Morny's usage + * + * contains: + * - [[DATE_TIME_PATTERN_FULL_MILLIS]] the standard date-time-millis [[String]] pattern format + * - [[formatDate]] convert UTC time millis (and hour-offset time zone) + * to normalized date-time-millis [[String]] + * - [[formatDuration]] convert millis duration to normalized duration [[String]] + * + */ +object CommonFormat { + + /** the standard date-time-millis [[String]] pattern format that Morny in use. + * + * pattern string is pattern of [[DateTimeFormatter]]. + */ + //noinspection ScalaWeakerAccess + val DATE_TIME_PATTERN_FULL_MILLIS = "yyyy-MM-dd HH:mm:ss:SSS" + + /** the formatted date-time-millis [[String]]. + * + * time is formatted by pattern [[DATE_TIME_PATTERN_FULL_MILLIS]]. + * + * @param timestamp millis timestamp. timestamp should be UTC alignment. + * + * @param utcOffset the hour offset of the time zone, the time-zone controls + * which local time describe will use. + * + * for example, timestamp [[0]] describes 1970-1-1 00:00:00 in + * UTC+0, so, use the `timestamp` `0` and `utfOffset` `0` will + * returns `"1970-1-1 00:00:00:000"`; however, at the same time, + * in UTC+8, the local time is 1970-1-1 08:00:00:000, so use + * the `timestamp` `0` and the `utcOffset` `8` will returns + * `"1970-1-1 08:00:00:000"` + * + * @return the time-zone local date-time-millis [[String]] describes the timestamp. + */ + def formatDate (timestamp: Long, utcOffset: Int): String = + DateTimeFormatter.ofPattern(DATE_TIME_PATTERN_FULL_MILLIS).format( + LocalDateTime.ofInstant( + Instant.ofEpochMilli(timestamp), + ZoneId.ofOffset("UTC", ZoneOffset.ofHours(utcOffset)) + ) + ) + + /** human readable [[String]] that describes the millis duration. + * + * {{{ + * scala> formatDuration(10) + * val res0: String = 10ms + * + * scala> formatDuration(3000001) + * val res1: String = 50min 0s 1ms + * + * scala> formatDuration(94179047901720L) + * val res2: String = 1090035d 6h 38min 21s 720ms + * }}} + * + * @param duration time duration, in milliseconds + * @return time duration, human readable + */ + def formatDuration (duration: Long): String = + val sb = new StringBuilder() + if (duration > 1000 * 60 * 60 * 24) sb ++= (duration / (1000 * 60 * 60 * 24)).toString ++= "d " + if (duration > 1000 * 60 * 60) sb ++= (duration / (1000 * 60 * 60) % 24).toString ++= "h " + if (duration > 1000 * 60) sb ++= (duration / (1000 * 60) % 60).toString ++= "min " + if (duration > 1000) sb ++= (duration / 1000 % 60).toString ++= "s " + sb ++= (duration % 1000).toString ++= "ms" + sb toString + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala b/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala new file mode 100644 index 0000000..0d469d4 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala @@ -0,0 +1,55 @@ +package cc.sukazyo.cono.morny.util + +/** Added the [[toHex]] method to [[Byte]] and [[Array]]`[`[[Byte]]`]`. + * + * the [[toHex]] method will takes [[Byte]] as a binary byte and convert + * it to the hex [[String]] that can describe the binary byte. there are + * always 2 digits unsigned hex number. + * + * for example, byte `0` is binary `0000 0000`, it will be converted to + * `"00"`, and the byte `-1` is binary `1111 1111` which corresponding + * `"ff"`. + * {{{ + * scala> 0.toByte.toHex + * val res6: String = 00 + * + * scala> 15.toByte.toHex + * val res10: String = 0f + * + * scala> -1.toByte.toHex + * val res7: String = ff + * }}} + * + * while converting byte array, the order is: the 1st element of the array + * will be put most forward, then the following added to the tail of hex string. + * {{{ + * scala> Array[Byte](0, 1, 2, 3).toHex + * val res5: String = 00010203 + * }}} + * + */ +object ConvertByteHex { + + extension (b: Byte) { + + /** convert the binary of the [[Byte]] contains to hex string. + * @see [[ConvertByteHex]] + */ + def toHex: String = (b >> 4 & 0xf).toHexString + (b & 0xf).toHexString + + } + + extension (data: Array[Byte]) { + + /** convert the binary of the [[Array]]`[`[[Byte]]`]` contains to hex string. + * + * @see [[ConvertByteHex]] + */ + def toHex: String = + val sb = StringBuilder() + for (b <- data) sb ++= (b toHex) + sb toString + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala b/src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala new file mode 100644 index 0000000..f1e146b --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala @@ -0,0 +1,28 @@ +package cc.sukazyo.cono.morny.util + +import java.io.{FileInputStream, IOException} +import java.security.{MessageDigest, NoSuchAlgorithmException} +import scala.util.Using + +/** + * @todo docs + * @todo some tests? + */ +object FileUtils { + + @throws[IOException|NoSuchAlgorithmException] + def getMD5Three (path: String): String = { + val buffer = Array.ofDim[Byte](8192) + var len = 0 + val algo = MessageDigest.getInstance("MD5") + Using (FileInputStream(path)) { stream => + len = stream.read(buffer) + while (len != -1) + algo update (buffer, 0, len) + len = stream.read(buffer) + } + import ConvertByteHex.toHex + algo.digest toHex + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala b/src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala new file mode 100644 index 0000000..fd7d19e --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala @@ -0,0 +1,13 @@ +package cc.sukazyo.cono.morny.util + +import okhttp3.MediaType + +/** some public values of [[okhttp3]] */ +object OkHttpPublic { + + /** predefined [[okhttp3]] [[MediaType]]s */ + object MediaTypes: + /** [[MediaType]] of [[https://en.wikipedia.org/wiki/JSON JSON]]. using encoding ''UTF-8'' */ + val JSON: MediaType = MediaType.get("application/json; charset=utf-8") + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala new file mode 100644 index 0000000..00f4a0f --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala @@ -0,0 +1,86 @@ +package cc.sukazyo.cono.morny.util + +import scala.collection.mutable.ArrayBuffer +import scala.util.boundary + +/** + * @todo docs + * @todo maybe there can have some encapsulation + */ +object UniversalCommand { + + opaque type StrictMode = Boolean + //noinspection ScalaWeakerAccess + val strict: StrictMode = true + //noinspection ScalaWeakerAccess + val lossy: StrictMode = false + + def apply (using strict: StrictMode = strict)(input: String): Array[String] = { + + val builder = ArrayBuffer.empty[String] + + extension (c: Char) { + private inline def isUnsupported: Boolean = + (c == '\n') || (c == '\r') + private inline def isSeparator: Boolean = + c == ' ' + private inline def isQuote: Boolean = + (c == '\'') || (c == '"') + private inline def isEscapeChar: Boolean = + c == '\\' + private inline def escapableInQuote: Boolean = + c.isQuote || c.isEscapeChar + private inline def escapable: Boolean = + c.escapableInQuote || c.isSeparator + } + + var arg = StringBuilder() + var i = 0 + while (i < input.length) { + if (input(i) isSeparator) { + if (arg nonEmpty) builder += arg.toString + arg = arg.empty + } else if (input(i) isQuote) { + val _inside_tag = input(i) + boundary { while (true) { + i=i+1 + if (i >= input.length) + if strict then throw IllegalArgumentException("UniversalCommand: unclosed quoted text") + else boundary.break() + if (input(i) == _inside_tag) + boundary.break() + else if (input(i) isUnsupported) && strict then + throw IllegalArgumentException("UniversalCommand: unsupported new-line") + else if (input(i) isQuote) && strict then + throw IllegalArgumentException("UniversalCommand: mixed \" and ' used") + else if (input(i) isEscapeChar) + if (i+1 >= input.length) && strict then + throw IllegalArgumentException("UniversalCommand: \\ in the end") + if ((i+1 < input.length) && (input(i+1) escapableInQuote)) + i=i+1 + arg += input(i) + else + arg += input(i) + }} + } else if ((input(i) isUnsupported) && strict) { + throw IllegalArgumentException("UniversalCommand: unsupported new-line") + } else if (input(i) isEscapeChar) { + if (i + 1 >= input.length) && strict then throw IllegalArgumentException("UniversalCommand: \\ in the end") + if ((i+1 < input.length) && (input(i+1) escapable)) + i=i+1 + arg += input(i) + } else { + arg += input(i) + } + i = i + 1 + } + if (arg nonEmpty) builder += arg.toString + + builder toArray + + } + + object Lossy: + def apply (input: String): Array[String] = UniversalCommand(using lossy)(input) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala new file mode 100644 index 0000000..5f11c38 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala @@ -0,0 +1,19 @@ +package cc.sukazyo.cono.morny.util + +import scala.annotation.targetName + +/** @todo some tests */ +object UseMath { + + extension (self: Int) { + + def over (other: Int): Double = self.toDouble / other + + } + + extension (self: Int) { + @targetName("pow") + def ** (other: Int): Double = Math.pow(self, other) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala new file mode 100644 index 0000000..b6eb096 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala @@ -0,0 +1,33 @@ +package cc.sukazyo.cono.morny.util + +import scala.language.implicitConversions +import scala.util.Random + +/** + * @todo some tests maybe? + * @todo use the using clauses to provide random instance + */ +object UseRandom { + + class ChancePossibility[T <: Any] (val one: T) (using possibility: Double) { + def nor[U] (another: U): T|U = + if Random.nextDouble < possibility then one else another + } + + given Conversion[ChancePossibility[Boolean], Boolean] with + def apply(in: ChancePossibility[Boolean]): Boolean = in nor !in.one + + extension (num: Double) { + + def chance_is[T <: Any] (one: T): ChancePossibility[T] = + ChancePossibility(one)(using num) + + } + + def rand_half: Boolean = Random.nextBoolean + + def rand_id: String = + import ConvertByteHex.toHex + Random nextBytes 6 toHex + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala new file mode 100644 index 0000000..feb3e99 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.util + +import scala.util.boundary + +/** Useful utils of select one specific value in the given values. + * + * contains: + * - [[select()]] can select one value which is not [[Null]]. + * + */ +object UseSelect { + + /** Select the non-null value in the given values. + * + * @tparam T The value's type. + * @param values Given values, may be a T value or [[Null]]. + * @return The first non-null value in the given values, or [[Null]] if + * there's no non-null value. + */ + def select [T] (values: T|Null*): T|Null = { + boundary[T|Null] { + for (i <- values) if i != null then boundary.break(i) + null + } + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/package.scala b/src/main/scala/cc/sukazyo/cono/morny/util/package.scala new file mode 100644 index 0000000..43bf99c --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/package.scala @@ -0,0 +1,22 @@ +package cc.sukazyo.cono.morny + +/** Utils that [[cc.sukazyo.cono.morny]]'s code used. + * + * contains: + * - [[tgapi Telegram API/Utils Extras]] + * - extensions of language standard + * - [[CommonEncrypt]] re-encapsulated some encrypt algorithms, and some normalized while encrypting. + * - [[CommonFormat]] provides some format methods normalized based on Morny usage standard. + * - [[ConvertByteHex]] extensions [[Byte]] and so on, make it easier converting binary data in it to a hex string. + * - [[UseMath]] scala style to make Math function easier to use + * - [[UseRandom]] scala style to use Random to generate something + * - external library extras + * - [[OkHttpPublic]] defines some static value for [[okhttp3]] + * - useful misc utils + * - [[FileUtils]] contains some easy-to-use file action. + * - [[UniversalCommand]] provides a easy way to get an args array from a string input. + * - others + * - [[BiliTool about Bilibili]] + * + */ +package object util {} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala new file mode 100644 index 0000000..209415a --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala @@ -0,0 +1,32 @@ +package cc.sukazyo.cono.morny.util.tgapi + +import cc.sukazyo.cono.morny.util.UniversalCommand + +class InputCommand private ( + val target: String|Null, + val command: String, + val args: Array[String] +) { + + override def toString: String = + s"{{$command}@{$target}#{${args.mkString}}" + +} + +object InputCommand { + + def apply (input: Array[String]): InputCommand = { + val _ex = input(0) split ("@", 2) + val _args = input drop 1 + new InputCommand( + if _ex.length == 1 then null else _ex(1), + _ex(0), + _args + ) + } + + //noinspection NoTailRecursionAnnotation + def apply (input: String): InputCommand = + InputCommand(UniversalCommand.Lossy(input)) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/Standardize.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/Standardize.scala new file mode 100644 index 0000000..2796c02 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/Standardize.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.util.tgapi + +object Standardize { + + val CHANNEL_SPEAKER_MAGIC_ID = 136817688 + + val MASK_BOTAPI_ID: Long = -1000000000000 + +} \ No newline at end of file diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/TelegramExtensions.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/TelegramExtensions.scala new file mode 100644 index 0000000..5e38eeb --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/TelegramExtensions.scala @@ -0,0 +1,68 @@ +package cc.sukazyo.cono.morny.util.tgapi + +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException +import com.pengrad.telegrambot.TelegramBot +import com.pengrad.telegrambot.model.{Chat, ChatMember, User} +import com.pengrad.telegrambot.request.{BaseRequest, GetChatMember} +import com.pengrad.telegrambot.response.BaseResponse + +import scala.annotation.targetName + +object TelegramExtensions { + + object Bot { extension (bot: TelegramBot) { + + def exec [T <: BaseRequest[T, R], R <: BaseResponse] (request: T, onError_message: String = ""): R = { + val response = bot execute request + if response isOk then return response + throw EventRuntimeException.ActionFailed( + if onError_message isEmpty then response.errorCode toString else onError_message, + response + ) + } + + }} + + object Chat { extension (chat: Chat) { + + def hasMember (user: User) (using TelegramBot): Boolean = + memberHasPermission(user, ChatMember.Status.member) + + def memberHasPermission (user: User, permission: ChatMember.Status) (using bot: TelegramBot): Boolean = { + + //noinspection ScalaUnusedSymbol + enum UserPermissionLevel(val level: Int): + private case CREATOR extends UserPermissionLevel(10) + private case ADMINISTRATOR extends UserPermissionLevel(3) + private case MEMBER extends UserPermissionLevel(1) + private case RESTRICTED extends UserPermissionLevel(-1) + private case LEFT extends UserPermissionLevel(-3) + private case KICKED extends UserPermissionLevel(-5) + @targetName("equalOrGreaterThan") + def >= (another: UserPermissionLevel): Boolean = this.level >= another.level + object UserPermissionLevel: + def apply(status: ChatMember.Status): UserPermissionLevel = + status match + case ChatMember.Status.creator => CREATOR + case ChatMember.Status.administrator => ADMINISTRATOR + case ChatMember.Status.member => MEMBER + case ChatMember.Status.restricted => RESTRICTED + case ChatMember.Status.left => LEFT + case ChatMember.Status.kicked => KICKED + def apply (chatMember: ChatMember): UserPermissionLevel = apply(chatMember.status) + + import Bot.* + val chatMember: ChatMember = (bot exec GetChatMember(chat.id, user.id)).chatMember + if chatMember eq null then false + else UserPermissionLevel(chatMember) >= UserPermissionLevel(permission) + + } + + }} + + class LimboUser (id: Long) extends User(id) + class LimboChat (val _id: Long) extends Chat() { + override val id: java.lang.Long = _id + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.scala new file mode 100644 index 0000000..b44e7be --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.util.tgapi.event + +import com.pengrad.telegrambot.response.BaseResponse + +class EventRuntimeException (message: String) extends RuntimeException(message) + +object EventRuntimeException { + class ActionFailed (message: String, val response: BaseResponse) extends EventRuntimeException(message) +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/NamingUtils.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/NamingUtils.scala new file mode 100644 index 0000000..c0bc94b --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/NamingUtils.scala @@ -0,0 +1,11 @@ +package cc.sukazyo.cono.morny.util.tgapi.formatting + +import cc.sukazyo.cono.morny.util.CommonEncrypt +import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + +object NamingUtils { + + def inlineQueryId (tag: String, taggedData: String = ""): String = + CommonEncrypt.MD5(tag+taggedData) toHex + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala new file mode 100644 index 0000000..48fd6ef --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala @@ -0,0 +1,86 @@ +package cc.sukazyo.cono.morny.util.tgapi.formatting + +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.tgapi.Standardize.MASK_BOTAPI_ID +import com.pengrad.telegrambot.model.{Chat, Message, User} +import com.pengrad.telegrambot.model.Chat.Type + +object TelegramFormatter { + + extension (chat: Chat) { + + def safe_name: String = chat.`type` match + case Type.Private => _connectName(chat.firstName, chat.lastName) + case _ => chat.title + + def safe_linkHTML: String = + if (chat.username == null) + chat.`type` match + // language=html + case Type.Private => s"@[u:${chat.id}]" + // language=html + case _ => s"@[c/${chat.id}]" + else s"@${h(chat.username)}" + + //noinspection ScalaWeakerAccess + def safe_firstnameRefHTML: String = + chat.`type` match + // language=html + case Type.Private => s"${h(chat.firstName)}" + // language=html + case _ => s"${h(chat.title)}" + + //noinspection ScalaWeakerAccess + def id_tdLib: Long = + if chat.id < 0 then (chat.id - MASK_BOTAPI_ID)abs else chat.id + + def typeTag: String = chat.`type` match + case Type.Private => "🔒" + case Type.group => "💭" + case Type.supergroup => "💬" + case Type.channel => "📢" + + } + + extension (user: User) { + + //noinspection ScalaWeakerAccess + def fullname: String = _connectName(user.firstName, user.lastName) + + def fullnameRefHTML: String = + // language=html + s"${h(user.fullname)}" + + //noinspection ScalaWeakerAccess + def firstnameRefHTML: String = + // language=html + s"${h(user.firstName)}" + + def toLogTag: String = + (if (user.username == null) user.fullname + " " else "@" + user.username) + + "[" + user.id + "]" + + } + + extension (m: Message) { + + def sender_id: Long = + if m.senderChat == null then m.from.id else m.senderChat.id + + def sender_firstnameRefHTML: String = + if (m.senderChat == null) + m.from.firstnameRefHTML + else m.senderChat.safe_firstnameRefHTML + + } + + private inline def _link_user (id: Long): String = + s"tg://user?id=$id" + + private inline def _link_chat (id: Long): String = + s"https://t.me/c/$id" + + private inline def _connectName (firstName: String, lastName: String): String = + firstName + (if lastName == null then "" else " " + lastName) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramParseEscape.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramParseEscape.scala new file mode 100644 index 0000000..4b27795 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramParseEscape.scala @@ -0,0 +1,12 @@ +package cc.sukazyo.cono.morny.util.tgapi.formatting + +object TelegramParseEscape { + + def escapeHtml (input: String): String = + var process = input + process = process.replaceAll("&", "&") + process = process.replaceAll("<", "<") + process = process.replaceAll(">", ">") + process + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala new file mode 100644 index 0000000..88a9501 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala @@ -0,0 +1,67 @@ +package cc.sukazyo.cono.morny.util.tgapi.formatting + +import com.pengrad.telegrambot.model.User +import okhttp3.{OkHttpClient, Request} + +import java.io.IOException +import scala.util.matching.Regex +import scala.util.Using + +object TelegramUserInformation { + + private val DC_QUERY_SOURCE_SITE = "https://t.me/" + private val DC_QUERY_PROCESSOR_REGEX: Regex = "(cdn[1-9]).tele(sco.pe|gram-cdn.org)"r + + private val httpClient = OkHttpClient() + + @throws[IllegalArgumentException|IOException] + def getDataCenterFromUser (username: String): String = { + val request = Request.Builder().url(DC_QUERY_SOURCE_SITE + username).build + Using (httpClient.newCall(request) execute) { response => + val body = response.body + if body eq null then "" + else DC_QUERY_PROCESSOR_REGEX.findFirstMatchIn(body.string) match + case Some(res) => res.group(1) + case None => "" + } get + } + + def getFormattedInformation (user: User): String = { + import TelegramParseEscape.escapeHtml as h + + val userInfo = StringBuilder() + + userInfo ++= // language=html + s"""userid : + |- ${user.id}""" + .stripMargin + userInfo ++= { + if (user.username eq null) // language=html + s""" + |username : null + |datacenter : not supported""" + .stripMargin + else // language=html + s""" + |username : + |- ${h(user.username)} + |datacenter : + |- ${h(getDataCenterFromUser(user.username))}""" + .stripMargin + } + userInfo ++= // language=html + s""" + |display name : + |- ${h(user.firstName)}${if user.lastName ne null then s"\n- ${h(user.lastName)}" else ""}""" + .stripMargin + if (user.languageCode ne null) userInfo ++= // language=html + s""" + |language-code : + |- ${user.languageCode}""" + .stripMargin + + userInfo toString + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/package.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/package.scala new file mode 100644 index 0000000..d646076 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/package.scala @@ -0,0 +1,4 @@ +package cc.sukazyo.cono.morny.util + +/** @todo docs */ +package object tgapi {} diff --git a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java deleted file mode 100644 index 1b6eea7..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java +++ /dev/null @@ -1,18 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.cono.morny.util.UniversalCommand; - -import java.util.*; - -public class MornyCLI { - - public static void main (String[] args) { - - Scanner line = new Scanner(System.in); - System.out.print("$ java -jar morny-coeur-"+GradleProjectConfigures.VERSION+".jar " ); - String x = line.nextLine(); - ServerMain.main(UniversalCommand.format(x)); - - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestBiliTool.java b/src/test/java/cc/sukazyo/cono/morny/util/TestBiliTool.java deleted file mode 100644 index 125d9c6..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestBiliTool.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import static cc.sukazyo.cono.morny.util.BiliTool.*; - -public class TestBiliTool { - - private static final String AV_BV_DATA_CSV = """ - 17x411w7KC, 170001 - 1Q541167Qg, 455017605 - 1mK4y1C7Bz, 882584971 - 1T24y197V2, 688730800 - """; - - @ParameterizedTest - @CsvSource(textBlock = AV_BV_DATA_CSV) - void testAvToBv (String bv, int av) { - Assertions.assertEquals(bv, toBv(av)); - } - - @ParameterizedTest - @CsvSource(textBlock = AV_BV_DATA_CSV) - void testBvToAv (String bv, int av) { - Assertions.assertEquals(av, toAv(bv)); - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java b/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java deleted file mode 100644 index 8fd9ed7..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java +++ /dev/null @@ -1,50 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.stream.Stream; - -import static cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex; -import static cc.sukazyo.cono.morny.util.CommonConvert.byteToHex; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -public class TestCommonConvert { - - @ParameterizedTest - @CsvSource(textBlock = """ - 0x00, 00 - 0x01, 01 - 0x20, 20 - 0x77, 77 - -0x60, a0 - 0x0a, 0a - -0x01, ff - -0x05, fb - """ - ) - void testByteToHex(byte source, String expected) { - assertEquals(expected, byteToHex(source)); - } - - public static Stream testByteArrayToHexProvider () { - return Stream.of( - arguments(new byte[]{0x00}, "00"), - arguments(new byte[]{(byte)0xff}, "ff"), - arguments(new byte[]{(byte)0xc3}, "c3"), - arguments(new byte[]{}, ""), - arguments(new byte[]{0x30,0x0a,0x00,0x04,(byte)0xb0,0x00}, "300a0004b000"), - arguments(new byte[]{0x00,0x00,0x0a,(byte)0xff,(byte)0xfc,(byte)0xab,(byte)0x00,0x04}, "00000afffcab0004"), - arguments(new byte[]{0x00,0x7c,0x11,0x28,(byte)0x88,(byte)0xa6,(byte)0xfc,0x30}, "007c112888a6fc30") - ); - } - @ParameterizedTest - @MethodSource("testByteArrayToHexProvider") - void testByteArrayToHex (byte[] raw, String expected) { - assertEquals(expected, byteArrayToHex(raw)); - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonEncrypt.java b/src/test/java/cc/sukazyo/cono/morny/util/TestCommonEncrypt.java deleted file mode 100644 index 429b163..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonEncrypt.java +++ /dev/null @@ -1,23 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import static cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex; -import static cc.sukazyo.cono.morny.util.CommonEncrypt.*; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class TestCommonEncrypt { - - @ParameterizedTest - @SuppressWarnings("UnnecessaryStringEscape") - @CsvSource(textBlock = """ - 28be57d368b75051da76c068a6733284, '莲子' - 9644c5cbae223013228cd528817ba4f5, '莲子\n' - d41d8cd98f00b204e9800998ecf8427e, '' - """) - void testHashMd5_String (String md5, String text) { - assertEquals(md5, byteArrayToHex(hashMd5(text))); - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonFormat.java b/src/test/java/cc/sukazyo/cono/morny/util/TestCommonFormat.java deleted file mode 100644 index d7b67fe..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonFormat.java +++ /dev/null @@ -1,36 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import static cc.sukazyo.cono.morny.util.CommonFormat.*; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class TestCommonFormat { - - @ParameterizedTest - @CsvSource(textBlock = """ - 1664646870402, 8, 2022-10-02 01:54:30:402 - 1, 8, 1970-01-01 08:00:00:001 - 0, -1, 1969-12-31 23:00:00:000 - """ - ) - void testFormatDate (long timestamp, int utfOffset, String expectedHumanReadableTime) { - assertEquals(expectedHumanReadableTime, formatDate(timestamp, utfOffset)); - } - - @ParameterizedTest - @CsvSource(textBlock = """ - 100, '100ms' - 3000, '3s 0ms' - 326117522, '3d 18h 35min 17s 522ms' - 53373805, 14h 49min 33s 805ms - """) -// -1, '-1ms' // WARN: maybe sometime an unexpected usage -// -194271974291, '-291ms' // -// """) // - void testFormatDuration (long durationMillis, String humanReadableDuration) { - assertEquals(humanReadableDuration, formatDuration(durationMillis)); - } - -} diff --git a/src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala b/src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala new file mode 100644 index 0000000..a5c01f8 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala @@ -0,0 +1,12 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.util.UniversalCommand + +import scala.io.StdIn + +@main def MornyCLI (): Unit = { + + print("$ java -jar morny-coeur-\"+MornySystem.VERSION_FULL+\".jar ") + ServerMain main UniversalCommand(StdIn readLine) + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala b/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala new file mode 100644 index 0000000..8d62c60 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala @@ -0,0 +1,10 @@ +package cc.sukazyo.cono.morny.test + +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should + +abstract class MornyTests extends AnyFreeSpec with should.Matchers { + + val pending_val = "[not-implemented]" + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala new file mode 100644 index 0000000..1ae8264 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala @@ -0,0 +1,68 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.prop.TableDrivenPropertyChecks + +import scala.util.Random + +class BiliToolTest extends MornyTests with TableDrivenPropertyChecks { + + private val examples = Table( + ("bv", "av"), + ("17x411w7KC", 170001L), + ("1Q541167Qg", 455017605L), + ("1mK4y1C7Bz", 882584971L), + ("1T24y197V2", 688730800L), + ) + + forAll (examples) { (bv, av) => s"while using av$av/BV$bv :" - { + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, toBv} + "av to bv works" in { toBv(av) shouldEqual bv } + "bv to av works" in { toAv(bv) shouldEqual av } + }} + + "BV with unsupported length :" - { + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalFormatException} + val examples = Table( + "bv", + "12345", + "12345678", + "123456789", +// "1234567890", length 10 which is supported + "1234567890a", + "1234567890ab", + "1234567890abcdef" + ) + forAll(examples) { bv => + s"length ${bv.length} should throws IllegalFormatException" in: + an [IllegalFormatException] should be thrownBy toAv(bv) + } + } + + "BV with special character :" - { + val examples = Table( + ("bv" , "contains_special"), + ("1mK4O1C7Bz", "O"), + ("1m04m1C7Bz", "0"), + ("1mK4O1I7Bz", "I"), + ("1mK4O1C7Bl", "l"), + ("1--4O1C7Bl", "[symbols]") + ) + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalFormatException} + forAll(examples) { (bv, with_sp) => + s"'$with_sp' should throws IllegalFormatException" in: + an [IllegalFormatException] should be thrownBy toAv(bv) + } + } + + "av/bv converting should be reversible" in { + for (_ <- 1 to 20) { + val rand_av = Random.between(0, 999999999L) + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, toBv} + val my_bv = toBv(rand_av) + toAv(my_bv) shouldEqual rand_av + toBv(toAv(my_bv)) shouldEqual my_bv + } + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonEncryptTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonEncryptTest.scala new file mode 100644 index 0000000..8557661 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonEncryptTest.scala @@ -0,0 +1,33 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.prop.TableDrivenPropertyChecks + +class CommonEncryptTest extends MornyTests with TableDrivenPropertyChecks { + + "while doing hash :" - { + + val examples = Table( + ("md5" , "text"), + ("28be57d368b75051da76c068a6733284", "莲子"), + ("9644c5cbae223013228cd528817ba4f5", "莲子\n"), + ("d41d8cd98f00b204e9800998ecf8427e", "") + ) + + import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5 + import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + forAll (examples) { (md5, text) => + s"while hashing text \"$text\" :" - { + + s"the MD5 value should be $md5" in { MD5(text).toHex shouldEqual md5 } + + "other algorithms" in pending + + } + } + + s"while hashing binary file $pending_val" in pending + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonFormatTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonFormatTest.scala new file mode 100644 index 0000000..33dac4f --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonFormatTest.scala @@ -0,0 +1,46 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} +import org.scalatest.prop.TableDrivenPropertyChecks + +class CommonFormatTest extends MornyTests with TableDrivenPropertyChecks { + + "while using #formatDate :" - { + + val examples = Table( + ("time_text" , "timestamp", "zone_offset"), + ("2022-10-02 01:54:30:402", 1664646870402L, 8), + ("1970-01-01 08:00:00:001", 1L, 8), + ("1969-12-31 23:00:00:000", 0L, -1), + ) + + forAll(examples) { (time_text, timestamp, zone_offset) => + s"time $time_text in TimeZone($zone_offset) should be UTC timestamp $timestamp" in: + formatDate(timestamp, zone_offset) shouldEqual time_text + } + + } + + "while using #formatDuration :" - { + + val examples = Table( + ("time_millis", "duration_text"), + (100L , "100ms"), + (3000L , "3s 0ms"), + (326117522L , "3d 18h 35min 17s 522ms"), + (53373805L , "14h 49min 33s 805ms"), + (3600001L , "1h 0min 0s 1ms") + ) + + forAll(examples) { (time_millis, duration_text) => + + s"duration ($time_millis) millis should be formatted to '$duration_text'" in: + formatDuration(time_millis) shouldEqual duration_text + 0 should equal (0) + + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/ConvertByteHexTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/ConvertByteHexTest.scala new file mode 100644 index 0000000..25aea93 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/ConvertByteHexTest.scala @@ -0,0 +1,45 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.prop.TableDrivenPropertyChecks + +class ConvertByteHexTest extends MornyTests with TableDrivenPropertyChecks { + + private val examples_hex = Table( + ("byte" , "hex"), + ( 0x00 toByte, "00"), + ( 0x01 toByte, "01"), + ( 0x20 toByte, "20"), + ( 0x77 toByte, "77"), + (-0x60 toByte, "a0"), + ( 0x0a toByte, "0a"), + (-0x01 toByte, "ff"), + ( 0xfb toByte, "fb"), + ) + + "while using Byte#toHex :" - forAll (examples_hex) ((byte, hex) => { + s"byte ($byte) should be hex '$hex''" in { + import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + (byte toHex) shouldEqual hex + } + }) + + private val examples_hexs = Table( + ("bytes", "hex"), + (Array[Byte](0x00), "00"), + (Array[Byte](0xff toByte), "ff"), + (Array[Byte](0xc3 toByte), "c3"), + (Array[Byte](), ""), + (Array[Byte](0x30,0x0a,0x00,0x04,0xb0.toByte,0x00), "300a0004b000"), + (Array[Byte](0x00,0x00,0x0a,0xff.toByte,0xfc.toByte,0xab.toByte,0x00.toByte,0x04), "00000afffcab0004"), + (Array[Byte](0x00,0x7c,0x11,0x28,0x88.toByte,0xa6.toByte,0xfc.toByte,0x30), "007c112888a6fc30"), + ) + + "while using Array[Byte]#toHex :" - forAll(examples_hexs) ((bytes, hex) => { + s"byte array(${bytes mkString ","}) should be hex string $hex" in { + import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + (bytes toHex) shouldEqual hex + } + }) + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/FileUtilsTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/FileUtilsTest.scala new file mode 100644 index 0000000..1873d12 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/FileUtilsTest.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests + +class FileUtilsTest extends MornyTests { + + "while getting the MD5 hash of a file :" in pending + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala new file mode 100644 index 0000000..abf5010 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala @@ -0,0 +1,120 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.matchers.should.Matchers +import org.scalatest.prop.TableDrivenPropertyChecks + +class UniversalCommandTest extends MornyTests with Matchers with TableDrivenPropertyChecks { + + "while formatting command from String :" - { + + import cc.sukazyo.cono.morny.util.UniversalCommand as Cmd + import cc.sukazyo.cono.morny.util.UniversalCommand.Lossy as Lmd + def whileLossy (info: String): String = "in lossy mode " + info + def whileStrict (info: String): String = "in strict mode" + info + + raw"args should be separated by (\u0020) ascii-space" in: + Cmd("a b c delta e") shouldEqual Array("a", "b", "c", "delta", "e") + Lmd("a b c delta e") shouldEqual Array("a", "b", "c", "delta", "e") + "args should not be separated by non-ascii spaces" in: + Cmd("tests ダタ セト") shouldEqual Array("tests", "ダタ セト") + Lmd("tests ダタ セト") shouldEqual Array("tests", "ダタ セト") + "multiple ascii-spaces should not generate empty arg in middle" in: + Cmd("tests some of data") shouldEqual Array("tests", "some", "of", "data") + Lmd("tests some of data") shouldEqual Array("tests", "some", "of", "data") + + """texts and ascii-spaces in '' should grouped in one arg""" in: + Cmd("""tests 'data set'""") shouldEqual Array("tests", "data set") + Lmd("""tests 'data set'""") shouldEqual Array("tests", "data set") + """texts and ascii-spaces in "" should grouped in one arg""" in : + Cmd("""tests "data set"""") shouldEqual Array("tests", "data set") + Lmd("""tests "data set"""") shouldEqual Array("tests", "data set") + """texts nested '' should grouped in one arg""" in : + Cmd("""tests some:'data set'.message is""") shouldEqual Array("tests", "some:data set.message", "is") + Lmd("""tests some:'data set'.message is""") shouldEqual Array("tests", "some:data set.message", "is") + """texts and "" nested '' should grouped in one arg""" in : + Cmd("""tests "arg1 x":'arg2 y' param""") shouldEqual Array("tests", "arg1 x:arg2 y", "param") + Lmd("""tests "arg1 x":"arg2 y" param""") shouldEqual Array("tests", "arg1 x:arg2 y", "param") + "with ' not closed" - { + whileStrict("should throws IllegalArgumentException") in: + an[IllegalArgumentException] should be thrownBy Cmd("""use 'it """) + whileLossy("should be cut at end") in: + Lmd("use 'it ") shouldEqual Array("use", "it ") + } + """mixed ' and """" - { + whileStrict("should throws IllegalArgumentsException") in: + an[IllegalArgumentException] should be thrownBy Cmd("""tests "data set' "of it'""") + whileLossy("should be seen as a normal character") in: + Lmd("""tests "data set' "of it'""") shouldEqual Array("tests", "data set' of", "it") + } + + raw"\ should escape itself" in: + Cmd(raw"input \\data") shouldEqual Array("input", "\\data") + Lmd(raw"input \\data") shouldEqual Array("input", "\\data") + raw"\ should escape ascii-space, makes it processed as a normal character" in: + Cmd(raw"input data\ set") shouldEqual Array("input", "data set") + Lmd(raw"input data\ set") shouldEqual Array("input", "data set") + raw"\ should escape ascii-space, makes it can be an arg body" in: + Cmd(raw"input \ some-thing") shouldEqual Array("input", " ", "some-thing") + Lmd(raw"input \ some-thing") shouldEqual Array("input", " ", "some-thing") + raw"""\ should escape "", makes it processed as a normal character""" in : + Cmd(raw"""use \"inputted""") shouldEqual Array("use", "\"inputted") + Lmd(raw"""use \"inputted""") shouldEqual Array("use", "\"inputted") + raw"\ should escape '', makes it processed as a normal character" in: + Cmd(raw"use \'inputted") shouldEqual Array("use", "'inputted") + Lmd(raw"use \'inputted") shouldEqual Array("use", "'inputted") + raw"\ should escape itself which inside a quoted scope" in: + Cmd(raw"use 'quoted \\ body'") shouldEqual Array("use", "quoted \\ body") + Lmd(raw"use 'quoted \\ body'") shouldEqual Array("use", "quoted \\ body") + raw"""\ should escape " which inside a "" scope""" in: + Cmd(raw"""in "quoted \" body" body""") shouldEqual Array("in", "quoted \" body", "body") + Lmd(raw"""in "quoted \" body" body""") shouldEqual Array("in", "quoted \" body", "body") + raw"""\ should escape ' which inside a "" scope""" in : + Cmd(raw"""in "not-quoted \' body" body""") shouldEqual Array("in", "not-quoted ' body", "body") + Lmd(raw"""in "not-quoted \' body" body""") shouldEqual Array("in", "not-quoted ' body", "body") + raw"""\ should escape ' which inside a '' scope""" in : + Cmd(raw"""in 'quoted \' body' body""") shouldEqual Array("in", "quoted ' body", "body") + Lmd(raw"""in 'quoted \' body' body""") shouldEqual Array("in", "quoted ' body", "body") + raw"""\ should escape " which inside a ' scope""" in : + Cmd(raw"""in 'not-quoted \" body' body""") shouldEqual Array("in", "not-quoted \" body", "body") + Lmd(raw"""in 'not-quoted \" body' body""") shouldEqual Array("in", "not-quoted \" body", "body") + raw"\ should not escape ascii-space which inside a quoted scope" in: + Cmd(raw"""'quoted \ do not escape' did""") shouldEqual Array(raw"quoted \ do not escape", "did") + Lmd(raw"""'quoted \ do not escape' did""") shouldEqual Array(raw"quoted \ do not escape", "did") + raw"with \ in the end" - { + whileStrict("should throws IllegalArgumentException") in: + an [IllegalArgumentException] should be thrownBy Cmd("something error!\\") + whileLossy("should seen as normal char") in: + Lmd("something error!\\") shouldEqual Array("something", "error!\\") + } + + + "with multi-line input" - { + whileStrict("should throws IllegalArgumentException") in: + an [IllegalArgumentException] should be thrownBy Cmd("something will\nhave a new line") + whileLossy("should keep new-line char origin like") in: + Lmd("something will\nhave a new line") shouldEqual Array("something", "will\nhave", "a", "new", "line") + } + + + val example_special_character = Table( + "char", + " ", + "\t", + "\\t", + "\\a", + "/", + "&&", + "\\u1234", + ) + forAll(example_special_character) { char => + s"input with special character ($char) should keep origin like" in { + Cmd(s"$char dataset data[$char]contains parsed") shouldEqual + Array(char, "dataset", s"data[$char]contains", "parsed") + Lmd(s"$char dataset data[$char]contains parsed") shouldEqual + Array(char, "dataset", s"data[$char]contains", "parsed") + } + } + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/InputCommandTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/InputCommandTest.scala new file mode 100644 index 0000000..3baa038 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/InputCommandTest.scala @@ -0,0 +1,22 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi + +import cc.sukazyo.cono.morny.test.MornyTests + +class InputCommandTest extends MornyTests { + + "while create new InputCommand :" - { + + s"while input is $pending_val:" - { + + s"command should be $pending_val" in pending + s"target should be $pending_val" in pending + + "args array should always exists" in pending + + s"args should parsed to array $pending_val" in pending + + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/NamingUtilsTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/NamingUtilsTest.scala new file mode 100644 index 0000000..883d8aa --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/NamingUtilsTest.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi.formatting + +import cc.sukazyo.cono.morny.test.MornyTests + +class NamingUtilsTest extends MornyTests { + + "while generating inline query result id :" - { + + "while not use no data :" - { + + "(different tag) should return different id" in pending + "(same tag) should return the same id" in pending + + } + + "while use data :" - { + + "(same tag) with (same data) should return the same id" in pending + "(same tag) with (different data) should return different id" in pending + "(different tag) with (same data) should return different id" in pending + "change tag and data position should return different id" in pending + + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramFormatterTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramFormatterTest.scala new file mode 100644 index 0000000..1fbd912 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramFormatterTest.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi.formatting + +import cc.sukazyo.cono.morny.test.MornyTests + +class TelegramFormatterTest extends MornyTests { + + "some test" in pending + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramParseEscapeTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramParseEscapeTest.scala new file mode 100644 index 0000000..eb3f7da --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramParseEscapeTest.scala @@ -0,0 +1,25 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi.formatting + +import cc.sukazyo.cono.morny.test.MornyTests + +class TelegramParseEscapeTest extends MornyTests { + + "while escape HTML document :" - { + + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + val any_other = "0ir0Q*%_\"ir[0\"#*I%T\"I{EtjpJGI{\")#W*IT}P%*IH#){#NIJB9-/q{$(Jg'9m]q|MH4j0hq}|+($NR{')}}" + + "& must be escaped" in: + h("a & b") shouldEqual "a & b" + "< and > must be escaped" in: + h("") shouldEqual "<data-error>" + "& and < and > must all be escaped" in: + h(" && ") shouldEqual "<some-a> && <some-b>" + "space and count should be kept" in: + h("\t<<<< \n") shouldEqual "\t<<<< \n" + "any others should kept origin like" in: + h(any_other) shouldEqual any_other + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramUserInformationTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramUserInformationTest.scala new file mode 100644 index 0000000..c839a5b --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramUserInformationTest.scala @@ -0,0 +1,26 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi.formatting + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.prop.TableDrivenPropertyChecks +import org.scalatest.tagobjects.{Network, Slow} + +class TelegramUserInformationTest extends MornyTests with TableDrivenPropertyChecks { + + private val examples_telegram_cdn = Table( + ("username", "cdn"), + ("Eyre_S", "cdn5"), + ) + + forAll(examples_telegram_cdn) ((username, cdn) => s"while user is @$username :" - { + + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation.* + + s"datacenter should be $cdn" taggedAs (Slow, Network) in: + getDataCenterFromUser(username) shouldEqual cdn + + "formatted data should as expected" in: + pending + + }) + +} diff --git a/src/test/scala/live/LiveMain.scala b/src/test/scala/live/LiveMain.scala new file mode 100644 index 0000000..2b7de09 --- /dev/null +++ b/src/test/scala/live/LiveMain.scala @@ -0,0 +1,9 @@ +package live + +import cc.sukazyo.cono.morny.test.utils.BiliToolTest + +@main def LiveMain (args: String*): Unit = { + + org.scalatest.run(BiliToolTest()) + +}