diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..31a97b08 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,235 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false +max_line_length = 420 + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +# ReSharper properties +resharper_max_array_initializer_elements_on_line = 50 +resharper_max_initializer_elements_on_line = 1 +resharper_wrap_array_initializer_style = chop_if_long diff --git a/.gitignore b/.gitignore index a4fe18bd..0b2bbdce 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ *.user *.userosscache *.sln.docstates +.idea +.DS_Store # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..df5809be --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,32 @@ + + + Elsa Workflows Community + 2023 + + https://github.com/elsa-workflows/elsa-core + https://github.com/elsa-workflows/elsa-core + git + + latest + enable + enable + + MIT + icon.png + + + https://v3.elsaworkflows.io/nuget-icon.png + + true + Default + latest + + + true + snupkg + true + + + 3.3.2 + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..93412d02 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,45 @@ + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Elsa.Integrations.sln b/Elsa.Integrations.sln new file mode 100644 index 00000000..1d4b0cbc --- /dev/null +++ b/Elsa.Integrations.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35527.113 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{527248D6-B851-4C8D-8667-E2FB0A91DABF}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{DE39E491-5DDC-40F4-91D5-7F5FAF0E1884}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + CONTRIBUTING.md = CONTRIBUTING.md + Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props + icon.png = icon.png + LICENSE = LICENSE + NuGet.Config = NuGet.Config + README.md = README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Slack", "src\Elsa.Integrations.Slack\Elsa.Integrations.Slack.csproj", "{9732E404-11B5-48DB-B5D9-97997F018830}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Tests", "src\Elsa.Integrations.Tests\Elsa.Integrations.Tests.csproj", "{DD8CE0DF-4B4C-4E12-A89D-B1D543FAD185}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9732E404-11B5-48DB-B5D9-97997F018830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9732E404-11B5-48DB-B5D9-97997F018830}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9732E404-11B5-48DB-B5D9-97997F018830}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9732E404-11B5-48DB-B5D9-97997F018830}.Release|Any CPU.Build.0 = Release|Any CPU + {DD8CE0DF-4B4C-4E12-A89D-B1D543FAD185}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD8CE0DF-4B4C-4E12-A89D-B1D543FAD185}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD8CE0DF-4B4C-4E12-A89D-B1D543FAD185}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD8CE0DF-4B4C-4E12-A89D-B1D543FAD185}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9732E404-11B5-48DB-B5D9-97997F018830} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {DD8CE0DF-4B4C-4E12-A89D-B1D543FAD185} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + EndGlobalSection +EndGlobal diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 00000000..e5fb919d --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 00000000..47e1cade Binary files /dev/null and b/icon.png differ diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 00000000..db78798a --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,20 @@ + + + + + + net8.0;net9.0 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/CreateChannel.cs b/src/Elsa.Integrations.Slack/Activities/Channels/CreateChannel.cs new file mode 100644 index 00000000..b6bd4ec7 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/CreateChannel.cs @@ -0,0 +1,58 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Creates a new channel in a Slack workspace. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Creates a new channel in the workspace.", + DisplayName = "Create Channel")] +[UsedImplicitly] +public class CreateChannel : SlackActivity +{ + /// + /// The name of the channel to create. + /// + [Input(Description = "The name of the channel to create.")] + public Input ChannelName { get; set; } = default!; + + /// + /// Whether to create the channel as private. + /// + [Input(Name = "Is Private", Description = "Whether to create the channel as private.")] + public Input IsPrivate { get; set; } = default!; + + /// + /// The ID of the workspace. + /// + [Input(Name = "Team Id", Description = "The ID of the workspace.")] + public Input? TeamId { get; set; } + + /// + /// The created channel information. + /// + [Output(Description = "The created channel information.")] + public Output Channel { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelName = context.Get(ChannelName)!; + bool isPrivate = context.Get(IsPrivate); + string? teamId = context.Get(TeamId); + + ISlackApiClient client = GetClient(context); + Conversation channel = await client.Conversations.Create(channelName, isPrivate, teamId); + + context.Set(Channel, channel); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/GetChannel.cs b/src/Elsa.Integrations.Slack/Activities/Channels/GetChannel.cs new file mode 100644 index 00000000..9b400504 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/GetChannel.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Retrieves information about a channel. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Gets information about a channel.", + DisplayName = "Get Channel")] +[UsedImplicitly] +public class GetChannel : SlackActivity +{ + /// + /// The ID of the channel to get information about. + /// + [Input(Name = "Channel Id", Description = "The ID of the channel to get information about.")] + public Input ChannelId { get; set; } = default!; + + /// + /// The channel information. + /// + [Output(Description = "The channel information.")] + public Output Channel { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + + ISlackApiClient client = GetClient(context); + Conversation channel = await client.Conversations.Info(channelId); + context.Set(Channel, channel); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/GetChannelMessage.cs b/src/Elsa.Integrations.Slack/Activities/Channels/GetChannelMessage.cs new file mode 100644 index 00000000..8cdae7c9 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/GetChannelMessage.cs @@ -0,0 +1,64 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.Events; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Retrieves a message from a private channel. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Gets a message from a private channel.", + DisplayName = "Get Private Channel Message")] +[UsedImplicitly] +public class GetChannelMessage : SlackActivity +{ + /// + /// The ID of the private channel containing the message. + /// + [Input(Name = "Channel Id", Description = "The ID of the private channel containing the message.")] + public Input ChannelId { get; set; } = default!; + + /// + /// Timestamp of the message to retrieve. + /// + [Input(Name = "Message Timestamp", Description = "Timestamp of the message to retrieve.")] + public Input MessageTs { get; set; } = default!; + + /// + /// The retrieved message. + /// + [Output(Description = "The retrieved message.")] + public Output Message { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + string messageTs = context.Get(MessageTs)!; + + ISlackApiClient client = GetClient(context); + ConversationHistoryResponse history = await client.Conversations.History( + channelId, + latestTs: messageTs, + limit: 1, + inclusive: true); + + MessageEvent? message = history.Messages.FirstOrDefault(m => m.Ts == messageTs); + + if (message == null) + { + throw new InvalidOperationException($"Message with timestamp {messageTs} not found in channel {channelId}"); + } + + context.Set(Message, message); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/KickUser.cs b/src/Elsa.Integrations.Slack/Activities/Channels/KickUser.cs new file mode 100644 index 00000000..20dc7baa --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/KickUser.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Kicks (removes) a user from a channel. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Removes a user from a channel.", + DisplayName = "Kick User From Channel")] +[UsedImplicitly] +public class KickUser : SlackActivity +{ + /// + /// The ID of the channel to remove the user from. + /// + [Input(Name = "Channel Id", Description = "The ID of the channel to remove the user from.")] + public Input ChannelId { get; set; } = default!; + + /// + /// The ID of the user to remove from channel. + /// + [Input(Name = "User Id", Description = "The ID of the user to remove from channel.")] + public Input UserId { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + string userId = context.Get(UserId)!; + + ISlackApiClient client = GetClient(context); + await client.Conversations.Kick(channelId, userId); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/LeaveChannel.cs b/src/Elsa.Integrations.Slack/Activities/Channels/LeaveChannel.cs new file mode 100644 index 00000000..bf3d3546 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/LeaveChannel.cs @@ -0,0 +1,36 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Leaves a channel. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Leaves a channel.", + DisplayName = "Leave Channel")] +[UsedImplicitly] +public class LeaveChannel : SlackActivity +{ + /// + /// The ID of the channel to leave. + /// + [Input(Name = "Channel Id", Description = "The ID of the channel to leave.")] + public Input ChannelId { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + + ISlackApiClient client = GetClient(context); + await client.Conversations.Leave(channelId); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/ListChannels.cs b/src/Elsa.Integrations.Slack/Activities/Channels/ListChannels.cs new file mode 100644 index 00000000..7dcd25b5 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/ListChannels.cs @@ -0,0 +1,66 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Lists all channels in a workspace. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Lists all channels in the workspace.", + DisplayName = "List Channels")] +[UsedImplicitly] +public class ListChannels : SlackActivity +{ + /// + /// Set to true to exclude archived channels from the list. + /// + [Input(Name = "Exclude Archived", Description = "Set to true to exclude archived channels from the list.")] + public Input ExcludeArchived { get; set; } = default!; + + /// + /// Number of channels to return per page. + /// + [Input(Description = "Number of channels to return per page.")] + public Input? Limit { get; set; } + + /// + /// The cursor for the next page of results. + /// + [Input(Description = "Paginate through collections of data by setting the cursor parameter to a next_cursor attribute returned by a previous request's response_metadata.")] + public Input? Cursor { get; set; } + + /// + /// The list of channels. + /// + [Output(Description = "The list of channels.")] + public Output> Channels { get; set; } = default!; + + /// + /// The cursor for the next page of results. + /// + [Output(Name = "Next Cursor", Description = "The cursor for the next page of results.")] + public Output NextCursor { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + bool excludeArchived = context.Get(ExcludeArchived); + int limit = context.Get(Limit) ?? 100; + string? cursor = context.Get(Cursor); + + ISlackApiClient client = GetClient(context); + ConversationListResponse response = await client.Conversations.List(excludeArchived, limit, cursor: cursor); + + context.Set(Channels, response.Channels); + context.Set(NextCursor, response.ResponseMetadata?.NextCursor); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/ListMembersInChannel.cs b/src/Elsa.Integrations.Slack/Activities/Channels/ListMembersInChannel.cs new file mode 100644 index 00000000..006b6e31 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/ListMembersInChannel.cs @@ -0,0 +1,70 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Lists members of a channel. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Lists all members in a channel.", + DisplayName = "List Channel Members")] +[UsedImplicitly] +public class ListMembersInChannel : SlackActivity +{ + /// + /// The ID of the channel to list members from. + /// + [Input(Name = "Channel Id", Description = "The ID of the channel to list members from.")] + public Input ChannelId { get; set; } = default!; + + /// + /// Number of members to return per page. + /// + [Input(Description = "Number of members to return per page.")] + public Input Limit { get; set; } = null!; + + /// + /// Paginate through collections of data by setting the cursor parameter to a next_cursor attribute returned by a previous request's response_metadata. + /// + [Input(Description = "Paginate through collections of data by setting the cursor parameter to a next_cursor attribute returned by a previous request's response_metadata.")] + public Input Cursor { get; set; } = null!; + + /// + /// The list of member IDs in the channel. + /// + [Output(Description = "The list of member IDs in the channel.")] + public Output> MemberIds { get; set; } = default!; + + /// + /// The cursor for the next page of results. + /// + [Output(Name = "Next Cursor", Description = "The cursor for the next page of results.")] + public Output NextCursor { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + int limit = context.Get(Limit) ?? 100; + string? cursor = context.Get(Cursor); + + ISlackApiClient client = GetClient(context); + ConversationMembersResponse response = await client.Conversations.Members( + channelId, + limit, + cursor, + context.CancellationToken); + + context.Set(MemberIds, response.Members); + context.Set(NextCursor, response.ResponseMetadata?.NextCursor); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/SetChannelPurpose.cs b/src/Elsa.Integrations.Slack/Activities/Channels/SetChannelPurpose.cs new file mode 100644 index 00000000..c0f37c29 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/SetChannelPurpose.cs @@ -0,0 +1,51 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Sets the purpose of a channel. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Sets the purpose for a channel.", + DisplayName = "Set Channel Purpose")] +[UsedImplicitly] +public class SetChannelPurpose : SlackActivity +{ + /// + /// The ID of the channel to set the purpose of. + /// + [Input(Name = "Channel Id", Description = "The ID of the channel to set the purpose of.")] + public Input ChannelId { get; set; } = default!; + + /// + /// The new purpose. + /// + [Input(Description = "The new purpose.")] + public Input Purpose { get; set; } = default!; + + /// + /// The updated purpose. + /// + [Output(Description = "The updated purpose.")] + public Output UpdatedPurpose { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + string purpose = context.Get(Purpose)!; + + ISlackApiClient client = GetClient(context); + string updatedPurpose = await client.Conversations.SetPurpose(channelId, purpose); + + context.Set(UpdatedPurpose, updatedPurpose); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Channels/SetChannelTopic.cs b/src/Elsa.Integrations.Slack/Activities/Channels/SetChannelTopic.cs new file mode 100644 index 00000000..fc6352aa --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Channels/SetChannelTopic.cs @@ -0,0 +1,51 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Channels; + +/// +/// Sets the topic of a channel. +/// +[Activity( + "Elsa.Integrations.Slack.Channels", + "Slack Channels", + "Sets the topic for a channel.", + DisplayName = "Set Channel Topic")] +[UsedImplicitly] +public class SetChannelTopic : SlackActivity +{ + /// + /// The ID of the channel to set the topic of. + /// + [Input(Name = "Channel Id", Description = "The ID of the channel to set the topic of.")] + public Input ChannelId { get; set; } = default!; + + /// + /// The new topic. + /// + [Input(Description = "The new topic.")] + public Input Topic { get; set; } = default!; + + /// + /// The updated topic. + /// + [Output(Description = "The updated topic.")] + public Output UpdatedTopic { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + string topic = context.Get(Topic)!; + + ISlackApiClient client = GetClient(context); + string updatedTopic = await client.Conversations.SetTopic(channelId, topic); + + context.Set(UpdatedTopic, updatedTopic); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Events/SlackEventActivity.cs b/src/Elsa.Integrations.Slack/Activities/Events/SlackEventActivity.cs new file mode 100644 index 00000000..dac8eaf6 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Events/SlackEventActivity.cs @@ -0,0 +1,13 @@ +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; + +namespace Elsa.Integrations.Slack.Activities.Events; + +/// +/// Base class for Slack event watching activities. +/// +public abstract class SlackEventActivity : SlackActivity +{ + [Input(Name = "Bot User Id", Description = "The ID of the bot user to filter out self-messages.")] + public Input BotUserId { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Events/WatchDirectMessages.cs b/src/Elsa.Integrations.Slack/Activities/Events/WatchDirectMessages.cs new file mode 100644 index 00000000..73d53eba --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Events/WatchDirectMessages.cs @@ -0,0 +1,28 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Events; + +/// +/// Triggers when a direct message is received. +/// +[Activity( + "Elsa.Integrations.Slack.Events", + "Slack Events", + "Triggers when a direct message is received.", + DisplayName = "Watch Direct Messages")] +[UsedImplicitly] +public class WatchDirectMessages : SlackEventActivity +{ + [Output(Description = "The received message.")] + public Output ReceivedMessage { get; set; } = default!; + + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // Implementation depends on Slack's Events API and WebSocket support + throw new NotImplementedException("Event subscription requires WebSocket implementation."); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Events/WatchFiles.cs b/src/Elsa.Integrations.Slack/Activities/Events/WatchFiles.cs new file mode 100644 index 00000000..073877e0 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Events/WatchFiles.cs @@ -0,0 +1,34 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using File = SlackNet.File; + +namespace Elsa.Integrations.Slack.Activities.Events; + +/// +/// Triggers when a new file is added. +/// +[Activity( + "Elsa.Integrations.Slack.Events", + "Slack Events", + "Triggers when a new file is added.", + DisplayName = "Watch Files")] +[UsedImplicitly] +public class WatchFiles : SlackEventActivity +{ + /// + /// The added file. + /// + [Output(Description = "The added file.")] + public Output AddedFile { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // Implementation depends on Slack's Events API and WebSocket support + throw new NotImplementedException("Event subscription requires WebSocket implementation."); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Events/WatchMultipartyDirectMessages.cs b/src/Elsa.Integrations.Slack/Activities/Events/WatchMultipartyDirectMessages.cs new file mode 100644 index 00000000..89eafccc --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Events/WatchMultipartyDirectMessages.cs @@ -0,0 +1,34 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Events; + +/// +/// Triggers when a message is added to a multiparty direct message. +/// +[Activity( + "Elsa.Integrations.Slack.Events", + "Slack Events", + "Triggers when a message is added to a multiparty direct message.", + DisplayName = "Watch Multiparty Direct Messages")] +[UsedImplicitly] +public class WatchMultipartyDirectMessages : SlackEventActivity +{ + /// + /// The received message. + /// + [Output(Description = "The received message.")] + public Output ReceivedMessage { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // Implementation depends on Slack's Events API and WebSocket support + throw new NotImplementedException("Event subscription requires WebSocket implementation."); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Events/WatchNewEvents.cs b/src/Elsa.Integrations.Slack/Activities/Events/WatchNewEvents.cs new file mode 100644 index 00000000..53a256a6 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Events/WatchNewEvents.cs @@ -0,0 +1,34 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Activities; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Slack.Activities.Events; + +/// +/// Triggers when any new event is created. +/// +[Activity( + "Elsa.Integrations.Slack.Events", + "Slack Events", + "Triggers when any new event is created.", + DisplayName = "Watch New Events")] +[UsedImplicitly] +public class WatchNewEvents : SlackEventActivity +{ + /// + /// The received event. + /// + [Output(Description = "The received event.")] + public Output ReceivedEvent { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // Implementation depends on Slack's Events API and WebSocket support + throw new NotImplementedException("Event subscription requires WebSocket implementation."); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Events/WatchPublicChannelMessages.cs b/src/Elsa.Integrations.Slack/Activities/Events/WatchPublicChannelMessages.cs new file mode 100644 index 00000000..d05b32b9 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Events/WatchPublicChannelMessages.cs @@ -0,0 +1,40 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Events; + +/// +/// Triggers when a message is added to a public channel. +/// +[Activity( + "Elsa.Integrations.Slack.Events", + "Slack Events", + "Triggers when a message is added to a public channel.", + DisplayName = "Watch Public Channel Messages")] +[UsedImplicitly] +public class WatchPublicChannelMessages : SlackEventActivity +{ + /// + /// The ID of the public channel to watch. + /// + [Input(Name = "Channel Id", Description = "The ID of the public channel to watch.")] + public Input ChannelId { get; set; } = default!; + + /// + /// The received message. + /// + [Output(Description = "The received message.")] + public Output ReceivedMessage { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // Implementation depends on Slack's Events API and WebSocket support + throw new NotImplementedException("Event subscription requires WebSocket implementation."); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Events/WatchUsers.cs b/src/Elsa.Integrations.Slack/Activities/Events/WatchUsers.cs new file mode 100644 index 00000000..82c54977 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Events/WatchUsers.cs @@ -0,0 +1,34 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Events; + +/// +/// Triggers when a user is added or changed. +/// +[Activity( + "Elsa.Integrations.Slack.Events", + "Slack Events", + "Triggers when a user is added or changed.", + DisplayName = "Watch Users")] +[UsedImplicitly] +public class WatchUsers : SlackEventActivity +{ + /// + /// The user that was added or changed. + /// + [Output(Description = "The user that was added or changed.")] + public Output ChangedUser { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // Implementation depends on Slack's Events API and WebSocket support + throw new NotImplementedException("Event subscription requires WebSocket implementation."); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Files/UploadFile.cs b/src/Elsa.Integrations.Slack/Activities/Files/UploadFile.cs new file mode 100644 index 00000000..501c2266 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Files/UploadFile.cs @@ -0,0 +1,71 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Files; + +/// +/// Uploads a file to Slack. +/// +[Activity( + "Elsa.Integrations.Slack.Files", + "Slack Files", + "Uploads a file to Slack.", + DisplayName = "Upload File")] +[UsedImplicitly] +public class UploadFile : SlackActivity +{ + /// + /// The file to upload. + /// + [Input(Description = "The file to upload.")] + public Input File { get; set; } = default!; + + /// + /// The channel ID where the file will be shared. If not specified the file will be private. + /// + [Input(Name = "Channel Id", Description = "Channel ID where the file will be shared. If not specified the file will be private.")] + public Input? ChannelId { get; set; } + + /// + /// Provide another message's timestamp value to upload this file as a reply. Never use a reply's ts value; use its parent instead. + /// + [Input(Name = "Thread Timestamp", Description = "Provide another message's timestamp value to upload this file as a reply. Never use a reply's ts value; use its parent instead.")] + public Input? ThreadTs { get; set; } + + /// + /// The message text introducing the file in specified channels. + /// + [Input(Name = "Initial Comment", Description = "The message text introducing the file in specified channels.")] + public Input? InitialComment { get; set; } + + /// + /// The reference to the uploaded file. + /// + [Output(Name = "File Reference", Description = "The reference to the uploaded file.")] + public Output FileReference { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + FileUpload file = context.Get(File)!; + string? channelId = context.Get(ChannelId); + string? threadTs = context.Get(ThreadTs); + string? initialComment = context.Get(InitialComment); + + ISlackApiClient client = GetClient(context); + ExternalFileReference fileReference = await client.Files.Upload( + file, + channelId, + threadTs, + initialComment, + context.CancellationToken); + + context.Set(FileReference, fileReference); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Messages/CreateMessage.cs b/src/Elsa.Integrations.Slack/Activities/Messages/CreateMessage.cs new file mode 100644 index 00000000..c1e6f4eb --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Messages/CreateMessage.cs @@ -0,0 +1,75 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Messages; + +/// +/// Sends a new message to a Slack channel. +/// +[Activity( + "Elsa.Integrations.Slack.Messages", + "Slack Messages", + "Sends a new message to a Slack channel.", + DisplayName = "Send Message")] +[UsedImplicitly] +public class CreateMessage : SlackActivity +{ + /// + /// The channel to send the message to. + /// + [Input(Description = "The channel to send the message to.")] + public Input Channel { get; set; } = default!; + + /// + /// The message text to send. + /// + [Input(Description = "The message text to send.")] + public Input Text { get; set; } = default!; + + /// + /// Optional thread timestamp to reply to a thread. + /// + [Input(Description = "Optional thread timestamp to reply to a thread.")] + public Input ThreadTimestamp { get; set; } = null!; + + /// + /// Whether to broadcast a reply to a thread to the channel. + /// + [Input(Description = "Whether to broadcast a reply to a thread to the channel.")] + public Input ReplyBroadcast { get; set; } = null!; + + /// + /// The timestamp of the sent message. + /// + [Output(Description = "The timestamp of the sent message.")] + public Output MessageTimestamp { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channel = context.Get(Channel)!; + string text = context.Get(Text)!; + string? threadTs = context.Get(ThreadTimestamp); + bool replyBroadcast = context.Get(ReplyBroadcast); + + ISlackApiClient client = GetClient(context); + + Message message = new() + { + Channel = channel, + Text = text, + ThreadTs = threadTs, + ReplyBroadcast = replyBroadcast + }; + + PostMessageResponse? response = await client.Chat.PostMessage(message); + + context.Set(MessageTimestamp, response?.Ts); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Messages/DeleteMessage.cs b/src/Elsa.Integrations.Slack/Activities/Messages/DeleteMessage.cs new file mode 100644 index 00000000..c04d67c9 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Messages/DeleteMessage.cs @@ -0,0 +1,44 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Messages; + +/// +/// Deletes a message from a Slack channel. +/// +[Activity( + "Elsa.Integrations.Slack.Messages", + "Slack Messages", + "Deletes a message from a Slack channel.", + DisplayName = "Delete Message")] +[UsedImplicitly] +public class DeleteMessage : SlackActivity +{ + /// + /// The channel containing the message. + /// + [Input(Description = "The channel containing the message.")] + public Input Channel { get; set; } = default!; + + /// + /// The timestamp of the message to delete. + /// + [Input(Description = "The timestamp of the message to delete.")] + public Input Timestamp { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channel = context.Get(Channel)!; + string ts = context.Get(Timestamp)!; + + ISlackApiClient client = GetClient(context); + + await client.Chat.Delete(channel, ts); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Messages/PinMessage.cs b/src/Elsa.Integrations.Slack/Activities/Messages/PinMessage.cs new file mode 100644 index 00000000..3e73d2a5 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Messages/PinMessage.cs @@ -0,0 +1,44 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Messages; + +/// +/// Pins a message to a channel. +/// +[Activity( + "Elsa.Integrations.Slack.Messages", + "Slack Messages", + "Pins a message to a channel.", + DisplayName = "Pin Message")] +[UsedImplicitly] +public class PinMessage : SlackActivity +{ + /// + /// The channel containing the message. + /// + [Input(Description = "The channel containing the message.")] + public Input Channel { get; set; } = default!; + + /// + /// The timestamp of the message to pin. + /// + [Input(Description = "The timestamp of the message to pin.")] + public Input Timestamp { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channel = context.Get(Channel)!; + string ts = context.Get(Timestamp)!; + + ISlackApiClient client = GetClient(context); + + await client.Pins.AddMessage(channel, ts); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Messages/UnpinMessage.cs b/src/Elsa.Integrations.Slack/Activities/Messages/UnpinMessage.cs new file mode 100644 index 00000000..adc91b12 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Messages/UnpinMessage.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Messages; + +/// +/// Unpins a message from a channel. +/// +[Activity( + "Elsa.Integrations.Slack.Messages", + "Slack Messages", + "Unpins a message from a channel.", + DisplayName = "Unpin Message")] +[UsedImplicitly] +public class UnpinMessage : SlackActivity +{ + /// + /// The channel containing the message. + /// + [Input(Description = "The channel containing the message.")] + public Input Channel { get; set; } = default!; + + /// + /// The timestamp of the message to unpin. + /// + [Input(Description = "The timestamp of the message to unpin.")] + public Input Timestamp { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channel = context.Get(Channel)!; + string ts = context.Get(Timestamp)!; + + ISlackApiClient client = GetClient(context); + await client.Pins.RemoveMessage(channel, ts); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Messages/UpdateMessage.cs b/src/Elsa.Integrations.Slack/Activities/Messages/UpdateMessage.cs new file mode 100644 index 00000000..e0e11aef --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Messages/UpdateMessage.cs @@ -0,0 +1,59 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Messages; + +/// +/// Updates an existing message in a Slack channel. +/// +[Activity( + "Elsa.Integrations.Slack.Messages", + "Slack Messages", + "Updates an existing message in a Slack channel.", + DisplayName = "Update Message")] +[UsedImplicitly] +public class UpdateMessage : SlackActivity +{ + /// + /// The channel ID containing the message. + /// + [Input(Description = "The channel ID containing the message.")] + public Input ChannelId { get; set; } = default!; + + /// + /// The timestamp of the message to update. + /// + [Input(Description = "The timestamp of the message to update.")] + public Input Timestamp { get; set; } = default!; + + /// + /// The new text for the message. + /// + [Input(Description = "The new text for the message.")] + public Input Text { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + string ts = context.Get(Timestamp)!; + string text = context.Get(Text)!; + + ISlackApiClient client = GetClient(context); + + MessageUpdate message = new() + { + ChannelId = channelId, + Text = text, + Ts = ts + }; + + await client.Chat.Update(message); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Reactions/AddReaction.cs b/src/Elsa.Integrations.Slack/Activities/Reactions/AddReaction.cs new file mode 100644 index 00000000..755d2aac --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Reactions/AddReaction.cs @@ -0,0 +1,51 @@ +using Elsa.Integrations.Slack.Services; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Reactions; + +/// +/// Adds a reaction to a message. +/// +[Activity( + "Elsa.Integrations.Slack.Reactions", + "Slack Reactions", + "Adds a reaction to a message.", + DisplayName = "Add Reaction")] +[UsedImplicitly] +public class AddReaction : SlackActivity +{ + /// + /// The name of the emoji to react with. + /// + [Input(Description = "The name of the emoji to react with.")] + public Input Emoji { get; set; } = default!; + + /// + /// The channel containing the message. + /// + [Input(Description = "The channel containing the message.")] + public Input Channel { get; set; } = default!; + + /// + /// The timestamp of the message to react to. + /// + [Input(Description = "The timestamp of the message to react to.")] + public Input Timestamp { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string emoji = context.Get(Emoji)!; + string channel = context.Get(Channel)!; + string timestamp = context.Get(Timestamp)!; + + ISlackApiClient client = GetClient(context); + await client.Reactions.AddToMessage(emoji, channel, timestamp); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Reactions/ListReactions.cs b/src/Elsa.Integrations.Slack/Activities/Reactions/ListReactions.cs new file mode 100644 index 00000000..797178f9 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Reactions/ListReactions.cs @@ -0,0 +1,44 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Reactions; + +/// +/// Lists reactions made by a user. +/// +[Activity( + "Elsa.Integrations.Slack.Reactions", + "Slack Reactions", + "Lists reactions made by a user.", + DisplayName = "List Reactions")] +[UsedImplicitly] +public class ListReactions : SlackActivity +{ + /// + /// The user to list reactions for. + /// + [Input(Name = "User Id", Description = "The user to list reactions for.")] + public Input UserId { get; set; } = default!; + + /// + /// The list of reactions made by the user. + /// + [Output(Name="User Reactions", Description="The list of reactions made by the user.")] + public Output> UserReactions { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string userId = context.Get(UserId)!; + + ISlackApiClient client = GetClient(context); + ReactionItemListResponse? response = await client.Reactions.List(userId, full: true); + context.Set(UserReactions, response?.Items); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Reactions/RemoveReaction.cs b/src/Elsa.Integrations.Slack/Activities/Reactions/RemoveReaction.cs new file mode 100644 index 00000000..1e3cd6e9 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Reactions/RemoveReaction.cs @@ -0,0 +1,50 @@ +using Elsa.Integrations.Slack.Services; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Reactions; + +/// +/// Removes a reaction from a message. +/// +[Activity( + "Elsa.Integrations.Slack.Reactions", + "Slack Reactions", + "Removes a reaction from a message.", + DisplayName = "Remove Reaction")] +[UsedImplicitly] +public class RemoveReaction : SlackActivity +{ + /// + /// The name of the emoji to remove. + /// + [Input(Description = "The name of the emoji to remove.")] + public Input Emoji { get; set; } = default!; + + /// + /// The channel containing the message. + /// + [Input(Description = "The channel containing the message.")] + public Input Channel { get; set; } = default!; + + /// + /// The timestamp of the message. + /// + [Input(Description = "The timestamp of the message.")] + public Input Timestamp { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string emoji = context.Get(Emoji)!; + string channel = context.Get(Channel)!; + string timestamp = context.Get(Timestamp)!; + ISlackApiClient client = GetClient(context); + await client.Reactions.RemoveFromMessage(emoji, channel, timestamp); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Reminders/CompleteReminder.cs b/src/Elsa.Integrations.Slack/Activities/Reminders/CompleteReminder.cs new file mode 100644 index 00000000..0e78ee90 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Reminders/CompleteReminder.cs @@ -0,0 +1,36 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Reminders; + +/// +/// Marks a reminder as complete. +/// +[Activity( + "Elsa.Integrations.Slack.Reminders", + "Slack Reminders", + "Marks a reminder as complete.", + DisplayName = "Complete Reminder")] +[UsedImplicitly] +public class CompleteReminder : SlackActivity +{ + /// + /// The ID of the reminder to mark as complete. + /// + [Input(Name = "Reminder Id", Description = "The ID of the reminder to mark as complete.")] + public Input ReminderId { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string reminderId = context.Get(ReminderId)!; + + ISlackApiClient client = GetClient(context); + await client.Reminders.Complete(reminderId); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Reminders/CreateReminder.cs b/src/Elsa.Integrations.Slack/Activities/Reminders/CreateReminder.cs new file mode 100644 index 00000000..e90aa4e3 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Reminders/CreateReminder.cs @@ -0,0 +1,57 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Reminders; + +/// +/// Creates a reminder. +/// +[Activity( + "Elsa.Integrations.Slack.Reminders", + "Slack Reminders", + "Creates a new reminder.", + DisplayName = "Create Reminder")] +[UsedImplicitly] +public class CreateReminder : SlackActivity +{ + /// + /// The text of the reminder. + /// + [Input(Description = "The text of the reminder.")] + public Input Text { get; set; } = default!; + + /// + /// When this reminder should happen: the Unix timestamp (up to 5 years from now), or a natural language description (e.g. 'in 15 minutes' or 'every Thursday at 3pm'). + /// + [Input(Description = "When this reminder should happen: the Unix timestamp (up to 5 years from now), or a natural language description (e.g. 'in 15 minutes' or 'every Thursday at 3pm').")] + public Input Time { get; set; } = default!; + + /// + /// The user who will receive this reminder. If not specified, defaults to the authed user. + /// + [Input(Name = "User Id", Description = "The user who will receive this reminder. If not specified, defaults to the authed user.")] + public Input? UserId { get; set; } + + /// + /// The ID of the created reminder. + /// + [Output(Name = "Reminder Id", Description = "The ID of the created reminder.")] + public Output ReminderId { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string text = context.Get(Text)!; + string time = context.Get(Time)!; + string? userId = context.Get(UserId); + + ISlackApiClient client = GetClient(context); + Reminder reminder = await client.Reminders.Add(text, time, userId); + context.Set(ReminderId, reminder.Id); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Reminders/DeleteReminder.cs b/src/Elsa.Integrations.Slack/Activities/Reminders/DeleteReminder.cs new file mode 100644 index 00000000..28d312c7 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Reminders/DeleteReminder.cs @@ -0,0 +1,36 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Reminders; + +/// +/// Deletes a reminder. +/// +[Activity( + "Elsa.Integrations.Slack.Reminders", + "Slack Reminders", + "Deletes a reminder.", + DisplayName = "Delete Reminder")] +[UsedImplicitly] +public class DeleteReminder : SlackActivity +{ + /// + /// The ID of the reminder to delete. + /// + [Input(Name = "Reminder Id", Description = "The ID of the reminder to delete.")] + public Input ReminderId { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string reminderId = context.Get(ReminderId)!; + + ISlackApiClient client = GetClient(context); + await client.Reminders.Delete(reminderId, CancellationToken.None); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Reminders/GetReminder.cs b/src/Elsa.Integrations.Slack/Activities/Reminders/GetReminder.cs new file mode 100644 index 00000000..66951b14 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Reminders/GetReminder.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Reminders; + +/// +/// Retrieves information about a reminder. +/// +[Activity( + "Elsa.Integrations.Slack.Reminders", + "Slack Reminders", + "Gets information about a specific reminder.", + DisplayName = "Get Reminder")] +[UsedImplicitly] +public class GetReminder : SlackActivity +{ + /// + /// The ID of the reminder to get information about. + /// + [Input(Name = "Reminder Id", Description = "The ID of the reminder to get information about.")] + public Input ReminderId { get; set; } = default!; + + /// + /// The reminder information. + /// + [Output(Name = "Reminder Info", Description = "The reminder information.")] + public Output ReminderInfo { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string reminderId = context.Get(ReminderId)!; + + ISlackApiClient client = GetClient(context); + Reminder reminder = await client.Reminders.Info(reminderId); + context.Set(ReminderInfo, reminder); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Reminders/ListReminders.cs b/src/Elsa.Integrations.Slack/Activities/Reminders/ListReminders.cs new file mode 100644 index 00000000..25343dc7 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Reminders/ListReminders.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Reminders; + +/// +/// Lists all reminders created by or for a given user. +/// +[Activity( + "Elsa.Integrations.Slack.Reminders", + "Slack Reminders", + "Lists all reminders for a user.", + DisplayName = "List Reminders")] +[UsedImplicitly] +public class ListReminders : SlackActivity +{ + /// + /// The user to list reminders for. If not specified, defaults to the authed user. + /// + [Input(Name = "User Id", Description = "The user to list reminders for. If not specified, defaults to the authed user.")] + public Input? UserId { get; set; } + + /// + /// The list of reminders. + /// + [Output(Description = "The list of reminders.")] + public Output> Reminders { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string? userId = context.Get(UserId); + + ISlackApiClient client = GetClient(context); + IReadOnlyList? response = await client.Reminders.List(); + context.Set(Reminders, response); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Search/SearchForMessage.cs b/src/Elsa.Integrations.Slack/Activities/Search/SearchForMessage.cs new file mode 100644 index 00000000..3c4ed646 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Search/SearchForMessage.cs @@ -0,0 +1,58 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Search; + +/// +/// Searches for messages matching a query. +/// +[Activity( + "Elsa.Integrations.Slack.Search", + "Slack Search", + "Searches for messages matching a query.", + DisplayName = "Search Messages")] +[UsedImplicitly] +public class SearchForMessage : SlackActivity +{ + /// + /// The search query. + /// + [Input(Description = "Search query - can include modifiers like 'in:#channel', 'from:@user', etc.")] + public Input Query { get; set; } = default!; + + /// + /// Number of results to return per page. + /// + [Input(Description = "Number of results to return per page.")] + public Input Count { get; set; } = null!; + + /// + /// Page number of results to return. + /// + [Input(Description = "Page number of results to return.")] + public Input Page { get; set; } = null!; + + /// + /// The search results. + /// + [Output(Description = "The search results.")] + public Output> Results { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string query = context.Get(Query)!; + int count = context.Get(Count) ?? 100; + int page = context.Get(Page) ?? 1; + + ISlackApiClient client = GetClient(context); + MessageSearchResponse results = await client.Search.Messages(query, count: count, page: page); + context.Set(Results, results.Messages); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/SlackActivity.cs b/src/Elsa.Integrations.Slack/Activities/SlackActivity.cs new file mode 100644 index 00000000..f66289b0 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/SlackActivity.cs @@ -0,0 +1,31 @@ +using Elsa.Integrations.Slack.Services; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities; + +/// +/// Generic base class inherited by all Slack activities. +/// +public abstract class SlackActivity : Activity +{ + /// + /// The Slack API token. + /// + [Input(Description = "The Slack API token.")] + public Input Token { get; set; } = default!; + + /// + /// Gets the Slack API client. + /// + /// The current context to get the client. + /// The Slack API client. + protected ISlackApiClient GetClient(ActivityExecutionContext context) + { + SlackClientFactory slackClientFactory = context.GetRequiredService(); + string token = context.Get(Token)!; + return slackClientFactory.GetClient(token); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/SlackTriggerActivity.cs b/src/Elsa.Integrations.Slack/Activities/SlackTriggerActivity.cs new file mode 100644 index 00000000..55030000 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/SlackTriggerActivity.cs @@ -0,0 +1,28 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; + +namespace Elsa.Integrations.Slack.Activities; + +/// +/// Base class for Slack event trigger activities. +/// +public abstract class SlackTriggerActivity : SlackActivity, ITrigger +{ + /// + /// The ID of the bot user to filter out self-messages. + /// + [Input(Name = "Bot User Id", Description = "The ID of the bot user to filter out self-messages.")] + public Input BotUserId { get; set; } = default!; + + /// + /// The ID of the channel to listen for messages in. + /// + public abstract string GetTriggerType(); + + /// + /// Returns the payloads to index. + /// + /// The trigger indexing context. + public abstract ValueTask> GetTriggerPayloadsAsync(TriggerIndexingContext context); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Stars/RemoveSavedItem.cs b/src/Elsa.Integrations.Slack/Activities/Stars/RemoveSavedItem.cs new file mode 100644 index 00000000..c547987f --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Stars/RemoveSavedItem.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Stars; + +/// +/// Removes an item from saved items. +/// +[Activity( + "Elsa.Integrations.Slack.SavedItems", + "Slack Saved Items", + "Removes a saved item.", + DisplayName = "Remove Saved Item")] +[UsedImplicitly] +public class RemoveSavedItem : SlackActivity +{ + /// + /// Channel where the saved item is located. + /// + [Input(Name = "Channel Id", Description = "Channel where the saved item is located.")] + public Input ChannelId { get; set; } = default!; + + /// + /// Timestamp of the message to remove from saved items. + /// + [Input(Description = "Timestamp of the message to remove from saved items.")] + public Input Timestamp { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + string timestamp = context.Get(Timestamp)!; + + ISlackApiClient client = GetClient(context); + await client.Pins.RemoveMessage(channelId, timestamp); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Stars/SaveItem.cs b/src/Elsa.Integrations.Slack/Activities/Stars/SaveItem.cs new file mode 100644 index 00000000..12e7a0d8 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Stars/SaveItem.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Stars; + +/// +/// Adds an item to saved items. +/// +[Activity( + "Elsa.Integrations.Slack.SavedItems", + "Slack Saved Items", + "Saves an item for later reference.", + DisplayName = "Save Item")] +[UsedImplicitly] +public class SaveItem : SlackActivity +{ + /// + /// Channel where the item is located. + /// + [Input(Name = "Channel Id", Description = "Channel where the item is located.")] + public Input ChannelId { get; set; } = default!; + + /// + /// Timestamp of the message to save. + /// + [Input(Description = "Timestamp of the message to save.")] + public Input Timestamp { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string channelId = context.Get(ChannelId)!; + string timestamp = context.Get(Timestamp)!; + + ISlackApiClient client = GetClient(context); + await client.Pins.AddMessage(channelId, timestamp); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Users/GetUser.cs b/src/Elsa.Integrations.Slack/Activities/Users/GetUser.cs new file mode 100644 index 00000000..de86676b --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Users/GetUser.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Users; + +/// +/// Retrieves details about a member of a workspace. +/// +[Activity( + "Elsa.Integrations.Slack.Users", + "Slack Users", + "Retrieves details about a member of a workspace.", + DisplayName = "Get User")] +[UsedImplicitly] +public class GetUser : SlackActivity +{ + /// + /// The ID of the user to get information about. + /// + [Input(Description = "The ID of the user to get information about.")] + public Input UserId { get; set; } = default!; + + /// + /// The user information. + /// + [Output(Description = "The user information.")] + public Output UserInfo { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string userId = context.Get(UserId)!; + + ISlackApiClient client = GetClient(context); + User user = await client.Users.Info(userId); + context.Set(UserInfo, user); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Users/ListUsers.cs b/src/Elsa.Integrations.Slack/Activities/Users/ListUsers.cs new file mode 100644 index 00000000..d185b052 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Users/ListUsers.cs @@ -0,0 +1,38 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; +using SlackNet.WebApi; + +namespace Elsa.Integrations.Slack.Activities.Users; + +/// +/// Lists all users in a workspace. +/// +[Activity( + "Elsa.Integrations.Slack.Users", + "Slack Users", + "Lists all users in a workspace.", + DisplayName = "List Users")] +[UsedImplicitly] +public class ListUsers : SlackActivity +{ + /// + /// The list of users in the workspace. + /// + [Output(Description = "The list of users in the workspace.")] + public Output> Users { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string token = context.Get(Token)!; + + ISlackApiClient client = GetClient(context); + UserListResponse users = await client.Users.List(); + context.Set(Users, users.Members); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Users/SearchForUser.cs b/src/Elsa.Integrations.Slack/Activities/Users/SearchForUser.cs new file mode 100644 index 00000000..7ad736c2 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Users/SearchForUser.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Users; + +/// +/// Searches for a user by email address. +/// +[Activity( + "Elsa.Integrations.Slack.Users", + "Slack Users", + "Searches for a user by email address.", + DisplayName = "Search User By Email")] +[UsedImplicitly] +public class SearchForUser : SlackActivity +{ + /// + /// The email address to search for. + /// + [Input(Description = "The email address to search for.")] + public Input Email { get; set; } = default!; + + /// + /// The found user information. + /// + [Output(Description = "The found user information.")] + public Output FoundUser { get; set; } = default!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string email = context.Get(Email)!; + + ISlackApiClient client = GetClient(context); + User user = await client.Users.LookupByEmail(email); + context.Set(FoundUser, user); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Activities/Users/SetStatus.cs b/src/Elsa.Integrations.Slack/Activities/Users/SetStatus.cs new file mode 100644 index 00000000..384f3c8f --- /dev/null +++ b/src/Elsa.Integrations.Slack/Activities/Users/SetStatus.cs @@ -0,0 +1,56 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using JetBrains.Annotations; +using SlackNet; + +namespace Elsa.Integrations.Slack.Activities.Users; + +/// +/// Sets a user's status. +/// +[Activity( + "Elsa.Integrations.Slack.Users", + "Slack Users", + "Sets a user's status.", + DisplayName = "Set Status")] +[UsedImplicitly] +public class SetStatus: SlackActivity +{ + /// + /// The status text to set. + /// + [Input(Description = "The status text to set.")] + public Input StatusText { get; set; } = default!; + + /// + /// The emoji to use for the status. + /// + [Input(Description = "The emoji to use for the status.")] + public Input StatusEmoji { get; set; } = default!; + + /// + /// Optional Unix timestamp for when the status should expire. + /// + [Input(Description = "Optional Unix timestamp for when the status should expire.")] + public Input StatusExpiration { get; set; } = null!; + + /// + /// Executes the activity. + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + string statusText = context.Get(StatusText)!; + string statusEmoji = context.Get(StatusEmoji)!; + long? statusExpiration = context.Get(StatusExpiration); + + ISlackApiClient client = GetClient(context); + + await client.UserProfile.Set(new UserProfile + { + StatusText = statusText, + StatusEmoji = statusEmoji, + StatusExpiration = statusExpiration ?? 0 + }); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Elsa.Integrations.Slack.csproj b/src/Elsa.Integrations.Slack/Elsa.Integrations.Slack.csproj new file mode 100644 index 00000000..8b9cd49c --- /dev/null +++ b/src/Elsa.Integrations.Slack/Elsa.Integrations.Slack.csproj @@ -0,0 +1,16 @@ + + + + true + + + + + + + + + + + + diff --git a/src/Elsa.Integrations.Slack/Features/SlackFeature.cs b/src/Elsa.Integrations.Slack/Features/SlackFeature.cs new file mode 100644 index 00000000..b66c18a3 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Features/SlackFeature.cs @@ -0,0 +1,19 @@ +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Elsa.Integrations.Slack.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Integrations.Slack.Features; + +/// +/// Represents a feature for setting up Slack integration within the Elsa framework. +/// +public class SlackFeature(IModule module) : FeatureBase(module) +{ + /// + /// Applies the feature to the specified service collection. + /// + public override void Apply() => + Services + .AddSingleton(); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/FodyWeavers.xml b/src/Elsa.Integrations.Slack/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Slack/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Slack/Services/SlackClientFactory.cs b/src/Elsa.Integrations.Slack/Services/SlackClientFactory.cs new file mode 100644 index 00000000..f7deb8a6 --- /dev/null +++ b/src/Elsa.Integrations.Slack/Services/SlackClientFactory.cs @@ -0,0 +1,37 @@ +using SlackNet; + +namespace Elsa.Integrations.Slack.Services; + +/// +/// Factory for creating Slack API clients. +/// +public class SlackClientFactory +{ + private readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly Dictionary _slackClients = new(); + + /// + /// Gets a Slack API client for the specified token. + /// + public ISlackApiClient GetClient(string token) + { + if (_slackClients.TryGetValue(token, out ISlackApiClient? client)) + return client; + + try + { + _semaphore.Wait(); + + if (_slackClients.TryGetValue(token, out client)) + return client; + + SlackApiClient newClient = new(token); + _slackClients[token] = newClient; + return newClient; + } + finally + { + _semaphore.Release(); + } + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Tests/Elsa.Integrations.Tests.csproj b/src/Elsa.Integrations.Tests/Elsa.Integrations.Tests.csproj new file mode 100644 index 00000000..e2b9a772 --- /dev/null +++ b/src/Elsa.Integrations.Tests/Elsa.Integrations.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/Elsa.Integrations.Tests/FodyWeavers.xml b/src/Elsa.Integrations.Tests/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Tests/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Tests/GlobalUsings.cs b/src/Elsa.Integrations.Tests/GlobalUsings.cs new file mode 100644 index 00000000..3334ee7f --- /dev/null +++ b/src/Elsa.Integrations.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using FluentAssertions; \ No newline at end of file diff --git a/src/Elsa.Integrations.Tests/Slack/Activities/Channels/CreateChannelTests.cs b/src/Elsa.Integrations.Tests/Slack/Activities/Channels/CreateChannelTests.cs new file mode 100644 index 00000000..92939b7c --- /dev/null +++ b/src/Elsa.Integrations.Tests/Slack/Activities/Channels/CreateChannelTests.cs @@ -0,0 +1,15 @@ +using Elsa.Integrations.Slack.Activities.Channels; + +namespace Elsa.Integrations.Tests.Slack.Activities.Channels; + +/// +/// Contains tests for the activity. +/// +public class CreateChannelTests +{ + /// + /// Tests the activity by creating a new channel in a Slack workspace. + /// + [Fact] + public void ExecuteAsync() => throw new NotImplementedException(); +} \ No newline at end of file