├── .editorconfig
├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ └── build.yml
├── .gitignore
├── Discord Stream Notify Bot.sln
├── Discord Stream Notify Bot
├── Auth
│ ├── TokenCrypto.cs
│ └── TokenManager.cs
├── Bot.cs
├── BotConfig.cs
├── Command
│ ├── Admin
│ │ ├── AdministraitonService.cs
│ │ └── Administration.cs
│ ├── Attribute
│ │ ├── CommandExampleAttribute.cs
│ │ ├── RequireGuildMemberCountAttribute.cs
│ │ └── RequireGuildOwnerAttribute.cs
│ ├── CommandHandler.cs
│ ├── CommonEqualityComparer.cs
│ ├── Extensions.cs
│ ├── Help
│ │ ├── Help.cs
│ │ └── HelpService.cs
│ ├── ICommandService.cs
│ ├── Normal
│ │ └── Normal.cs
│ ├── ReactionEventWrapper.cs
│ ├── TopLevelModule.cs
│ ├── TwitCasting
│ │ └── TwitCasting.cs
│ ├── Twitch
│ │ └── Twitch.cs
│ ├── Twitter
│ │ └── TwitterSpaces.cs
│ ├── Youtube
│ │ ├── YoutubeChannelSpider.cs
│ │ └── YoutubeStream.cs
│ └── YoutubeMember
│ │ └── YoutubeMember.cs
├── Data
│ └── HelpDescription.txt
├── DataBase
│ ├── MainDbContext.cs
│ ├── MainDbService.cs
│ └── Table
│ │ ├── BannerChange.cs
│ │ ├── DbEntity.cs
│ │ ├── GuildConfig.cs
│ │ ├── GuildYoutubeMemberConfig.cs
│ │ ├── HoloVideo.cs
│ │ ├── NijisanjiVideo.cs
│ │ ├── NonApprovedVideo.cs
│ │ ├── NoticeTwitcastingStreamChannel.cs
│ │ ├── NoticeTwitchStreamChannel.cs
│ │ ├── NoticeTwitterSpaceChannel.cs
│ │ ├── NoticeYoutubeStreamChannel.cs
│ │ ├── OtherVideo.cs
│ │ ├── RecordYoutubeChannel.cs
│ │ ├── TwitcastingSpider.cs
│ │ ├── TwitcastingStream.cs
│ │ ├── TwitchSpider.cs
│ │ ├── TwitchStream.cs
│ │ ├── TwitterSpace.cs
│ │ ├── TwitterSpaecSpider.cs
│ │ ├── Video.cs
│ │ ├── YoutubeChannelNameToId.cs
│ │ ├── YoutubeChannelOwnedType.cs
│ │ ├── YoutubeChannelSpider.cs
│ │ ├── YoutubeMemberAccessToken.cs
│ │ └── YoutubeMemberCheck.cs
├── Discord Stream Notify Bot.csproj
├── HttpClients
│ ├── DiscordWebhookClient.cs
│ ├── TwitCasting
│ │ └── TwitcastingClient.cs
│ ├── Twitcasting
│ │ └── Model
│ │ │ ├── CategoriesJson.cs
│ │ │ ├── GetAllRegistedWebHookJson.cs
│ │ │ ├── GetMovieInfoResponse.cs
│ │ │ ├── GetUserInfoResponse.cs
│ │ │ └── TcBackendStreamData.cs
│ └── Twitter
│ │ ├── TwitterClient.cs
│ │ ├── TwitterSpacesData.cs
│ │ └── TwitterUserJson.cs
├── Interaction
│ ├── Attribute
│ │ ├── CommandExampleAttribute.cs
│ │ ├── CommandSummaryAttribute.cs
│ │ ├── RequireGuildAttribute.cs
│ │ ├── RequireGuildMemberCountAttribute.cs
│ │ └── RequireGuildOwnerAttribute.cs
│ ├── CommonEqualityComparer.cs
│ ├── Extensions.cs
│ ├── Help
│ │ ├── Help.cs
│ │ └── Service
│ │ │ └── HelpService.cs
│ ├── IInteractionService.cs
│ ├── InteractionHandler.cs
│ ├── OwnerOnly
│ │ ├── SendMsgToAllGuild.cs
│ │ └── Service
│ │ │ └── SendMsgToAllGuildService.cs
│ ├── ReactionEventWrapper.cs
│ ├── TopLevelModule.cs
│ ├── Twitcasting
│ │ ├── Twitcasting.cs
│ │ └── TwitcastingSpider.cs
│ ├── Twitch
│ │ ├── Twitch.cs
│ │ └── TwitchSpider.cs
│ ├── Twitter
│ │ ├── TwitterSpaces.cs
│ │ └── TwitterSpacesSpider.cs
│ ├── Utility
│ │ ├── Service
│ │ │ └── UtilityService.cs
│ │ └── Utility.cs
│ ├── Youtube
│ │ ├── Youtube.cs
│ │ └── YoutubeChannelSpider.cs
│ └── YoutubeMember
│ │ ├── YoutubeMember.cs
│ │ └── YoutubeMemberSetting.cs
├── Log.cs
├── Logo.png
├── Logo_128.ico
├── Logo_64.ico
├── Migrations
│ ├── 20250320095452_RefactorDbContext.Designer.cs
│ ├── 20250320095452_RefactorDbContext.cs
│ ├── 20250603065853_ModifyTwitCastingTable.Designer.cs
│ ├── 20250603065853_ModifyTwitCastingTable.cs
│ └── MainDbContextModelSnapshot.cs
├── Program.cs
├── RedisConnection.cs
├── RedisDataStore.cs
├── SharedService
│ ├── EmojiService.cs
│ ├── TwitCasting
│ │ └── TwitCastingService.cs
│ ├── Twitcasting
│ │ └── TwitCastingWebHookJson.cs
│ ├── Twitch
│ │ ├── Debounce
│ │ │ └── DebounceChannelUpdateMessage.cs
│ │ └── TwitchService.cs
│ ├── Twitter
│ │ └── TwitterSpacesService.cs
│ ├── Youtube
│ │ ├── ChangeGuildBanner.cs
│ │ ├── Json
│ │ │ ├── NijisanjiLiverJson.cs
│ │ │ └── NijisanjiStreamJson.cs
│ │ ├── ReminderAction.cs
│ │ ├── Schedule.cs
│ │ └── YoutubeStreamService.cs
│ └── YoutubeMember
│ │ ├── CheckMemberShip.cs
│ │ ├── CheckMemberShipOnlyVideoId.cs
│ │ └── YoutubeMemberService.cs
├── UptimeKumaClient.cs
└── Utility.cs
├── LICENSE.txt
└── README.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # Default severity for analyzer diagnostics with category 'Style'
4 | dotnet_analyzer_diagnostic.category-Style.severity = silent
5 |
6 | # CA1050: 在命名空間中宣告類型
7 | dotnet_diagnostic.CA1050.severity = none
8 | csharp_space_around_binary_operators = before_and_after
9 | csharp_indent_labels = one_less_than_current
10 | csharp_using_directive_placement = outside_namespace:silent
11 | csharp_style_conditional_delegate_call = true:suggestion
12 | csharp_style_var_for_built_in_types = false:silent
13 | csharp_style_var_when_type_is_apparent = false:silent
14 | csharp_style_var_elsewhere = false:silent
15 | csharp_prefer_static_local_function = true:suggestion
16 | csharp_prefer_static_anonymous_function = true:suggestion
17 | csharp_style_prefer_readonly_struct = true:suggestion
18 | csharp_style_prefer_readonly_struct_member = true:suggestion
19 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
20 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
21 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
22 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
23 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
24 |
25 | [*.{cs,vb}]
26 | end_of_line = crlf
27 | tab_width = 4
28 | indent_size = 4
29 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
30 | dotnet_style_qualification_for_field = false:silent
31 | dotnet_style_qualification_for_property = false:silent
32 | dotnet_style_qualification_for_method = false:silent
33 | dotnet_style_qualification_for_event = false:silent
34 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
35 | dotnet_code_quality_unused_parameters = all:suggestion
36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
39 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
40 | dotnet_style_allow_multiple_blank_lines_experimental = true:silent
41 | dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
42 | dotnet_style_readonly_field = true:suggestion
43 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent
44 | dotnet_style_predefined_type_for_member_access = true:silent
45 | dotnet_style_coalesce_expression = true:suggestion
46 | dotnet_style_null_propagation = true:suggestion
47 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
48 | dotnet_style_prefer_auto_properties = true:silent
49 | dotnet_style_object_initializer = true:suggestion
50 | dotnet_style_collection_initializer = true:suggestion
51 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
52 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
53 | dotnet_style_prefer_conditional_expression_over_return = true:silent
54 | dotnet_style_explicit_tuple_names = true:suggestion
55 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
56 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
57 | dotnet_style_prefer_compound_assignment = true:suggestion
58 | dotnet_style_prefer_simplified_interpolation = true:suggestion
59 | dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
60 | dotnet_style_namespace_match_folder = true:suggestion
61 | [*.cs]
62 | #### 命名樣式 ####
63 |
64 | # 命名規則
65 |
66 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
67 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
68 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
69 |
70 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
71 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
72 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
73 |
74 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
75 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
76 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
77 |
78 | # 符號規格
79 |
80 | dotnet_naming_symbols.interface.applicable_kinds = interface
81 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
82 | dotnet_naming_symbols.interface.required_modifiers =
83 |
84 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
85 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
86 | dotnet_naming_symbols.types.required_modifiers =
87 |
88 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
89 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
90 | dotnet_naming_symbols.non_field_members.required_modifiers =
91 |
92 | # 命名樣式
93 |
94 | dotnet_naming_style.begins_with_i.required_prefix = I
95 | dotnet_naming_style.begins_with_i.required_suffix =
96 | dotnet_naming_style.begins_with_i.word_separator =
97 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
98 |
99 | dotnet_naming_style.pascal_case.required_prefix =
100 | dotnet_naming_style.pascal_case.required_suffix =
101 | dotnet_naming_style.pascal_case.word_separator =
102 | dotnet_naming_style.pascal_case.capitalization = pascal_case
103 |
104 | dotnet_naming_style.pascal_case.required_prefix =
105 | dotnet_naming_style.pascal_case.required_suffix =
106 | dotnet_naming_style.pascal_case.word_separator =
107 | dotnet_naming_style.pascal_case.capitalization = pascal_case
108 | csharp_style_prefer_switch_expression = true:suggestion
109 | csharp_style_prefer_pattern_matching = true:silent
110 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
111 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
112 | csharp_style_prefer_not_pattern = true:suggestion
113 | csharp_style_prefer_extended_property_pattern = true:suggestion
114 | csharp_prefer_simple_using_statement = true:suggestion
115 | csharp_prefer_braces = true:silent
116 | csharp_style_namespace_declarations = block_scoped:silent
117 | csharp_style_prefer_method_group_conversion = true:silent
118 | csharp_style_prefer_top_level_statements = true:silent
119 | csharp_style_prefer_primary_constructors = true:suggestion
120 | csharp_prefer_system_threading_lock = true:suggestion
121 | csharp_style_expression_bodied_methods = false:silent
122 | csharp_style_expression_bodied_constructors = false:silent
123 | csharp_style_expression_bodied_operators = false:silent
124 | csharp_style_expression_bodied_properties = true:silent
125 | csharp_style_expression_bodied_indexers = true:silent
126 | csharp_style_expression_bodied_accessors = true:silent
127 | csharp_style_expression_bodied_lambdas = true:silent
128 | csharp_style_expression_bodied_local_functions = false:silent
129 | csharp_style_throw_expression = true:suggestion
130 | csharp_style_prefer_null_check_over_type_check = true:suggestion
131 | csharp_prefer_simple_default_expression = true:suggestion
132 | csharp_style_prefer_local_over_anonymous_function = true:suggestion
133 | csharp_style_prefer_index_operator = true:suggestion
134 | csharp_style_prefer_range_operator = true:suggestion
135 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
136 | csharp_style_prefer_tuple_swap = true:suggestion
137 | csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
138 | csharp_style_prefer_utf8_string_literals = true:suggestion
139 | csharp_style_inlined_variable_declaration = true:suggestion
140 | csharp_style_deconstructed_variable_declaration = true:suggestion
141 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion
142 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent
143 |
144 | [*.vb]
145 | #### 命名樣式 ####
146 |
147 | # 命名規則
148 |
149 | dotnet_naming_rule.interface_should_be_以_i_開頭.severity = suggestion
150 | dotnet_naming_rule.interface_should_be_以_i_開頭.symbols = interface
151 | dotnet_naming_rule.interface_should_be_以_i_開頭.style = 以_i_開頭
152 |
153 | dotnet_naming_rule.型別_should_be_pascal_命名法的大小寫.severity = suggestion
154 | dotnet_naming_rule.型別_should_be_pascal_命名法的大小寫.symbols = 型別
155 | dotnet_naming_rule.型別_should_be_pascal_命名法的大小寫.style = pascal_命名法的大小寫
156 |
157 | dotnet_naming_rule.非欄位成員_should_be_pascal_命名法的大小寫.severity = suggestion
158 | dotnet_naming_rule.非欄位成員_should_be_pascal_命名法的大小寫.symbols = 非欄位成員
159 | dotnet_naming_rule.非欄位成員_should_be_pascal_命名法的大小寫.style = pascal_命名法的大小寫
160 |
161 | # 符號規格
162 |
163 | dotnet_naming_symbols.interface.applicable_kinds = interface
164 | dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
165 | dotnet_naming_symbols.interface.required_modifiers =
166 |
167 | dotnet_naming_symbols.型別.applicable_kinds = class, struct, interface, enum
168 | dotnet_naming_symbols.型別.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
169 | dotnet_naming_symbols.型別.required_modifiers =
170 |
171 | dotnet_naming_symbols.非欄位成員.applicable_kinds = property, event, method
172 | dotnet_naming_symbols.非欄位成員.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
173 | dotnet_naming_symbols.非欄位成員.required_modifiers =
174 |
175 | # 命名樣式
176 |
177 | dotnet_naming_style.以_i_開頭.required_prefix = I
178 | dotnet_naming_style.以_i_開頭.required_suffix =
179 | dotnet_naming_style.以_i_開頭.word_separator =
180 | dotnet_naming_style.以_i_開頭.capitalization = pascal_case
181 |
182 | dotnet_naming_style.pascal_命名法的大小寫.required_prefix =
183 | dotnet_naming_style.pascal_命名法的大小寫.required_suffix =
184 | dotnet_naming_style.pascal_命名法的大小寫.word_separator =
185 | dotnet_naming_style.pascal_命名法的大小寫.capitalization = pascal_case
186 |
187 | dotnet_naming_style.pascal_命名法的大小寫.required_prefix =
188 | dotnet_naming_style.pascal_命名法的大小寫.required_suffix =
189 | dotnet_naming_style.pascal_命名法的大小寫.word_separator =
190 | dotnet_naming_style.pascal_命名法的大小寫.capitalization = pascal_case
191 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: 'konnokai'
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 |
9 | jobs:
10 | build:
11 | name: Build
12 | runs-on: windows-latest
13 | permissions: read-all
14 | steps:
15 | - uses: actions/checkout@v2
16 | with:
17 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
18 | - name: Set up JDK 17
19 | uses: actions/setup-java@v1
20 | with:
21 | java-version: 17
22 | - name: Cache SonarQube packages
23 | uses: actions/cache@v1
24 | with:
25 | path: ~\.sonar\cache
26 | key: ${{ runner.os }}-sonar
27 | restore-keys: ${{ runner.os }}-sonar
28 | - name: Cache SonarQube scanner
29 | id: cache-sonar-scanner
30 | uses: actions/cache@v1
31 | with:
32 | path: .\.sonar\scanner
33 | key: ${{ runner.os }}-sonar-scanner
34 | restore-keys: ${{ runner.os }}-sonar-scanner
35 | - name: Install SonarQube scanner
36 | if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
37 | shell: powershell
38 | run: |
39 | New-Item -Path .\.sonar\scanner -ItemType Directory
40 | dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
41 | - name: Build and analyze
42 | shell: powershell
43 | run: |
44 | .\.sonar\scanner\dotnet-sonarscanner begin /k:"konnokai_Discord-Stream-Notify-Bot_6d6ba123-be32-4ed6-87e9-13e25e542aca" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="${{ secrets.SONAR_HOST_URL }}"
45 | dotnet build
46 | .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
364 |
365 | /Discord Stream Notify Bot/DbTrackerLog.txt
--------------------------------------------------------------------------------
/Discord Stream Notify Bot.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.2.32526.322
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord Stream Notify Bot", "Discord Stream Notify Bot\Discord Stream Notify Bot.csproj", "{A3022865-83A3-4A54-AB3D-E4589F705146}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C7EE682C-42B5-40F4-B959-F717BBBF72A8}"
9 | ProjectSection(SolutionItems) = preProject
10 | .editorconfig = .editorconfig
11 | LICENSE.txt = LICENSE.txt
12 | README.md = README.md
13 | EndProjectSection
14 | EndProject
15 | Global
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug_API|Any CPU = Debug_API|Any CPU
18 | Debug_DontRegisterCommand|Any CPU = Debug_DontRegisterCommand|Any CPU
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {A3022865-83A3-4A54-AB3D-E4589F705146}.Debug_API|Any CPU.ActiveCfg = Debug_API|Any CPU
24 | {A3022865-83A3-4A54-AB3D-E4589F705146}.Debug_API|Any CPU.Build.0 = Debug_API|Any CPU
25 | {A3022865-83A3-4A54-AB3D-E4589F705146}.Debug_DontRegisterCommand|Any CPU.ActiveCfg = Debug_DontRegisterCommand|Any CPU
26 | {A3022865-83A3-4A54-AB3D-E4589F705146}.Debug_DontRegisterCommand|Any CPU.Build.0 = Debug_DontRegisterCommand|Any CPU
27 | {A3022865-83A3-4A54-AB3D-E4589F705146}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {A3022865-83A3-4A54-AB3D-E4589F705146}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {A3022865-83A3-4A54-AB3D-E4589F705146}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {A3022865-83A3-4A54-AB3D-E4589F705146}.Release|Any CPU.Build.0 = Release|Any CPU
31 | EndGlobalSection
32 | GlobalSection(SolutionProperties) = preSolution
33 | HideSolutionNode = FALSE
34 | EndGlobalSection
35 | GlobalSection(ExtensibilityGlobals) = postSolution
36 | SolutionGuid = {C43BB15A-2E55-458A-AA66-C0B60165014B}
37 | EndGlobalSection
38 | EndGlobal
39 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Auth/TokenCrypto.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography;
2 | using System.Text;
3 |
4 | namespace Discord_Stream_Notify_Bot.Auth
5 | {
6 | public class TokenCrypto
7 | {
8 | //產生 HMACSHA256 雜湊
9 | public static string ComputeHMACSHA256(string data, string key)
10 | {
11 | var keyBytes = Encoding.UTF8.GetBytes(key);
12 | using (var hmacSHA = new HMACSHA256(keyBytes))
13 | {
14 | var dataBytes = Encoding.UTF8.GetBytes(data);
15 | var hash = hmacSHA.ComputeHash(dataBytes, 0, dataBytes.Length);
16 | return BitConverter.ToString(hash).Replace("-", "").ToUpper();
17 | }
18 | }
19 |
20 | //AES 加密
21 | public static string AESEncrypt(string data, string key, string iv)
22 | {
23 | var keyBytes = Encoding.UTF8.GetBytes(key);
24 | var ivBytes = Encoding.UTF8.GetBytes(iv);
25 | var dataBytes = Encoding.UTF8.GetBytes(data);
26 | using (var aes = Aes.Create())
27 | {
28 | aes.Key = keyBytes;
29 | aes.IV = ivBytes;
30 | aes.Mode = CipherMode.CBC;
31 | aes.Padding = PaddingMode.PKCS7;
32 | var encryptor = aes.CreateEncryptor();
33 | var encrypt = encryptor
34 | .TransformFinalBlock(dataBytes, 0, dataBytes.Length);
35 | return Convert.ToBase64String(encrypt);
36 | }
37 | }
38 |
39 | //AES 解密
40 | public static string AESDecrypt(string data, string key, string iv)
41 | {
42 | var keyBytes = Encoding.UTF8.GetBytes(key);
43 | var ivBytes = Encoding.UTF8.GetBytes(iv);
44 | var dataBytes = Convert.FromBase64String(data);
45 | using (var aes = Aes.Create())
46 | {
47 | aes.Key = keyBytes;
48 | aes.IV = ivBytes;
49 | aes.Mode = CipherMode.CBC;
50 | aes.Padding = PaddingMode.PKCS7;
51 | var decryptor = aes.CreateDecryptor();
52 | var decrypt = decryptor
53 | .TransformFinalBlock(dataBytes, 0, dataBytes.Length);
54 | return Encoding.UTF8.GetString(decrypt);
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Auth/TokenManager.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace Discord_Stream_Notify_Bot.Auth
4 | {
5 | public class TokenManager
6 | {
7 | ///
8 | /// 產生加密使用者資料
9 | ///
10 | /// 尚未加密的使用者資料
11 | /// 已加密的使用者資料
12 | public static string CreateToken(object data, string key)
13 | {
14 | var json = JsonConvert.SerializeObject(data);
15 | var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
16 | var iv = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16);
17 |
18 | //使用 AES 加密 Payload
19 | var encrypt = TokenCrypto
20 | .AESEncrypt(base64, key.Substring(0, 16), iv);
21 |
22 | //取得簽章
23 | var signature = TokenCrypto
24 | .ComputeHMACSHA256(iv + "." + encrypt, key.Substring(0, 64));
25 |
26 | return iv + "." + encrypt + "." + signature;
27 | }
28 |
29 | ///
30 | /// 解密Google Token Response資料
31 | ///
32 | /// 已加密的Google Token Response資料
33 | /// 未加密的Google Token Response資料
34 | /// Token格式錯誤
35 | /// 簽章驗證失敗
36 | public static T GetTokenResponseValue(string token, string key)
37 | {
38 | if (string.IsNullOrWhiteSpace(token)) return default(T);
39 |
40 | token = token.Replace(" ", "+");
41 | var split = token.Split('.');
42 | if (split.Length != 3) throw new ArgumentOutOfRangeException("token");
43 |
44 | var iv = split[0];
45 | var encrypt = split[1];
46 | var signature = split[2];
47 |
48 | //檢查簽章是否正確
49 | if (signature != TokenCrypto.ComputeHMACSHA256(iv + "." + encrypt, key.Substring(0, 64)))
50 | throw new ArgumentException(signature);
51 |
52 | //使用 AES 解密 Payload
53 | var base64 = TokenCrypto.AESDecrypt(encrypt, key.Substring(0, 16), iv);
54 | var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64));
55 | var payload = JsonConvert.DeserializeObject(json);
56 |
57 | return payload;
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/BotConfig.cs:
--------------------------------------------------------------------------------
1 | using Discord_Stream_Notify_Bot;
2 |
3 | public class BotConfig
4 | {
5 | public string MySqlConnectionString { get; set; } = "Server=localhost;Port=3306;User Id=stream_bot;Password=Ch@nge_Me;Database=discord_stream_bot";
6 | public string RedisOption { get; set; } = "127.0.0.1,syncTimeout=3000";
7 | public string RedisTokenKey { get; set; } = "";
8 |
9 | public string ApiServerDomain { get; set; } = "";
10 | public string UptimeKumaPushUrl { get; set; } = "";
11 |
12 | public string DiscordToken { get; set; } = "";
13 | public ulong TestSlashCommandGuildId { get; set; } = 0;
14 | public string WebHookUrl { get; set; } = "";
15 |
16 | public string GoogleApiKey { get; set; } = "";
17 | public string GoogleClientId { get; set; } = "";
18 | public string GoogleClientSecret { get; set; } = "";
19 |
20 | public string TwitCastingClientId { get; set; } = "";
21 | public string TwitCastingClientSecret { get; set; } = "";
22 | public string TwitCastingRecordPath { get; set; } = "";
23 |
24 | // https://streamlink.github.io/cli/plugins/twitch.html#authentication
25 | // 先放著,未來可能會用到
26 | public string TwitchCookieAuthToken { get; set; } = "";
27 | public string TwitchClientId { get; set; } = "";
28 | public string TwitchClientSecret { get; set; } = "";
29 |
30 | public string TwitterAuthToken { get; set; } = "";
31 | public string TwitterCSRFToken { get; set; } = "";
32 | public string TwitterSpaceRecordPath { get; set; } = "";
33 |
34 | public ulong YouTubeEmoteId { get; set; } = 1265158558299848827;
35 | public ulong PayPalEmoteId { get; set; } = 1265158658015236107;
36 | public ulong ECPayEmoteId { get; set; } = 1379272194210795622;
37 |
38 | public void InitBotConfig()
39 | {
40 | try { File.WriteAllText("bot_config_example.json", JsonConvert.SerializeObject(new BotConfig(), Formatting.Indented)); } catch { }
41 | if (!File.Exists("bot_config.json"))
42 | {
43 | Log.Error($"bot_config.json 遺失,請依照 {Path.GetFullPath("bot_config_example.json")} 內的格式填入正確的數值");
44 | if (!Console.IsInputRedirected)
45 | Console.ReadKey();
46 | Environment.Exit(3);
47 | }
48 |
49 | var config = JsonConvert.DeserializeObject(File.ReadAllText("bot_config.json"));
50 |
51 | try
52 | {
53 | if (string.IsNullOrWhiteSpace(config.DiscordToken))
54 | {
55 | Log.Error($"{nameof(DiscordToken)} 遺失,請輸入至 bot_config.json 後重開 Bot");
56 | if (!Console.IsInputRedirected)
57 | Console.ReadKey();
58 | Environment.Exit(3);
59 | }
60 |
61 | if (string.IsNullOrWhiteSpace(config.WebHookUrl))
62 | {
63 | Log.Error($"{nameof(WebHookUrl)} 遺失,請輸入至 bot_config.json 後重開 Bot");
64 | if (!Console.IsInputRedirected)
65 | Console.ReadKey();
66 | Environment.Exit(3);
67 | }
68 |
69 | if (string.IsNullOrWhiteSpace(config.GoogleApiKey))
70 | {
71 | Log.Error($"{nameof(GoogleApiKey)} 遺失,請輸入至 bot_config.json 後重開 Bot");
72 | if (!Console.IsInputRedirected)
73 | Console.ReadKey();
74 | Environment.Exit(3);
75 | }
76 |
77 | if (string.IsNullOrWhiteSpace(config.ApiServerDomain))
78 | {
79 | Log.Error($"{nameof(ApiServerDomain)} 遺失,請輸入至 bot_config.json 後重開 Bot");
80 | if (!Console.IsInputRedirected)
81 | Console.ReadKey();
82 | Environment.Exit(3);
83 | }
84 |
85 | MySqlConnectionString = config.MySqlConnectionString;
86 | RedisOption = config.RedisOption;
87 | RedisTokenKey = config.RedisTokenKey;
88 | ApiServerDomain = config.ApiServerDomain;
89 | DiscordToken = config.DiscordToken;
90 | WebHookUrl = config.WebHookUrl;
91 | GoogleApiKey = config.GoogleApiKey;
92 | TestSlashCommandGuildId = config.TestSlashCommandGuildId;
93 | TwitCastingClientId = config.TwitCastingClientId;
94 | TwitCastingClientSecret = config.TwitCastingClientSecret;
95 | TwitCastingRecordPath = config.TwitCastingRecordPath;
96 | TwitchCookieAuthToken = config.TwitchCookieAuthToken;
97 | TwitchClientId = config.TwitchClientId;
98 | TwitchClientSecret = config.TwitchClientSecret;
99 | TwitterAuthToken = config.TwitterAuthToken;
100 | TwitterCSRFToken = config.TwitterCSRFToken;
101 | TwitterSpaceRecordPath = config.TwitterSpaceRecordPath;
102 | GoogleClientId = config.GoogleClientId;
103 | GoogleClientSecret = config.GoogleClientSecret;
104 | UptimeKumaPushUrl = config.UptimeKumaPushUrl;
105 | YouTubeEmoteId = config.YouTubeEmoteId;
106 | PayPalEmoteId = config.PayPalEmoteId;
107 | ECPayEmoteId = config.ECPayEmoteId;
108 |
109 | if (string.IsNullOrWhiteSpace(config.RedisTokenKey) || string.IsNullOrWhiteSpace(RedisTokenKey))
110 | {
111 | Log.Error($"{nameof(RedisTokenKey)} 遺失,將重新建立隨機亂數");
112 |
113 | RedisTokenKey = GenRandomKey();
114 |
115 | try { File.WriteAllText("bot_config.json", JsonConvert.SerializeObject(this, Formatting.Indented)); }
116 | catch (Exception ex)
117 | {
118 | Log.Error($"設定檔保存失敗: {ex}");
119 | Log.Error($"請手動將此字串填入設定檔中的 \"{nameof(RedisTokenKey)}\" 欄位: {RedisTokenKey}");
120 | Environment.Exit(3);
121 | }
122 | }
123 |
124 | Utility.RedisKey = RedisTokenKey;
125 | }
126 | catch (Exception ex)
127 | {
128 | Log.Error($"設定檔讀取失敗: {ex}");
129 | throw;
130 | }
131 | }
132 |
133 | public static string GenRandomKey(int length = 128)
134 | {
135 | var characters = "ABCDEF_GHIJKLMNOPQRSTUVWXYZ@abcdefghijklmnopqrstuvwx-yz0123456789";
136 | var Charsarr = new char[128];
137 | var random = new Random();
138 |
139 | for (int i = 0; i < Charsarr.Length; i++)
140 | {
141 | Charsarr[i] = characters[random.Next(characters.Length)];
142 | }
143 |
144 | var resultString = new string(Charsarr);
145 | resultString = resultString[Math.Min(length, resultString.Length)..];
146 | return resultString;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/Admin/AdministraitonService.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.Command.Admin
2 | {
3 | public class AdministrationService : ICommandService
4 | {
5 | private string _reloadOfficialGuildListKey = "DiscordStreamBot:Admin:ReloadOfficialGuildList";
6 | private readonly DiscordSocketClient _Client;
7 |
8 | public AdministrationService(DiscordSocketClient client)
9 | {
10 | _Client = client;
11 |
12 | Bot.RedisSub.Subscribe(new RedisChannel(_reloadOfficialGuildListKey, RedisChannel.PatternMode.Literal), (_, _) =>
13 | {
14 | try
15 | {
16 | Utility.OfficialGuildList = JsonConvert.DeserializeObject>(File.ReadAllText(Utility.GetDataFilePath("OfficialList.json")));
17 | }
18 | catch (Exception ex)
19 | {
20 | Log.Error(ex.Demystify(), "ReloadOfficialGuildList Error");
21 | }
22 | });
23 | }
24 |
25 | public async Task ClearUser(ITextChannel textChannel)
26 | {
27 | IEnumerable msgs = (await textChannel.GetMessagesAsync(100).FlattenAsync().ConfigureAwait(false))
28 | .Where((item) => item.Author.Id == _Client.CurrentUser.Id);
29 |
30 | await Task.WhenAll(Task.Delay(1000), textChannel.DeleteMessagesAsync(msgs)).ConfigureAwait(false);
31 | }
32 |
33 | internal bool WriteAndReloadOfficialListFile()
34 | {
35 | try
36 | {
37 | File.WriteAllText(Utility.GetDataFilePath("OfficialList.json"), JsonConvert.SerializeObject(Utility.OfficialGuildList));
38 | Bot.RedisSub.Publish(new RedisChannel(_reloadOfficialGuildListKey, RedisChannel.PatternMode.Literal), "");
39 | }
40 | catch (Exception ex)
41 | {
42 | Log.Error(ex.Demystify(), "WriteOfficialListFile Error");
43 | return false;
44 | }
45 |
46 | return true;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/Attribute/CommandExampleAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.Command.Attribute
2 | {
3 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
4 | sealed class CommandExampleAttribute : System.Attribute
5 | {
6 | readonly string[] expArray;
7 |
8 | public CommandExampleAttribute(params string[] expArray)
9 | {
10 | this.expArray = expArray;
11 | }
12 |
13 | public string[] ExpArray
14 | {
15 | get { return expArray; }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/Attribute/RequireGuildMemberCountAttribute.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 |
3 | namespace Discord_Stream_Notify_Bot.Command.Attribute
4 | {
5 | public class RequireGuildMemberCountAttribute : PreconditionAttribute
6 | {
7 | public RequireGuildMemberCountAttribute(uint gCount)
8 | {
9 | GuildMemberCount = gCount;
10 | }
11 |
12 | public uint? GuildMemberCount { get; }
13 | public override string ErrorMessage { get; set; } = "此伺服器不可使用本指令";
14 |
15 | public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
16 | {
17 | if (context.Message.Author.Id == Bot.ApplicatonOwner.Id) return Task.FromResult(PreconditionResult.FromSuccess());
18 |
19 | if (Utility.OfficialGuildList.Contains(context.Guild.Id)) return Task.FromResult(PreconditionResult.FromSuccess());
20 |
21 | var memberCount = ((SocketGuild)context.Guild).MemberCount;
22 | if (memberCount >= GuildMemberCount) return Task.FromResult(PreconditionResult.FromSuccess());
23 | else return Task.FromResult(PreconditionResult.FromError($"此伺服器不可使用本指令\n" +
24 | $"指令要求伺服器人數: `{GuildMemberCount}` 人\n" +
25 | $"目前 Bot 所取得的伺服器人數: `{memberCount}` 人\n" +
26 | $"由於快取的關係,可能會遇到伺服器人數錯誤的問題\n" +
27 | $"如有任何需要請聯繫 Bot 擁有者處理 (你可使用 `/utility send-message-to-bot-owner` 對擁有者發送訊息)"));
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/Attribute/RequireGuildOwnerAttribute.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 |
3 | namespace Discord_Stream_Notify_Bot.Command.Attribute
4 | {
5 | public class RequireGuildOwnerAttribute : PreconditionAttribute
6 | {
7 | public RequireGuildOwnerAttribute()
8 | {
9 | }
10 |
11 | public override string ErrorMessage { get; set; } = "非伺服器擁有者不可使用本指令";
12 |
13 | public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
14 | {
15 | if (context.Message.Author.Id == Bot.ApplicatonOwner.Id) return Task.FromResult(PreconditionResult.FromSuccess());
16 |
17 | if (context.Message.Author.Id == context.Guild.OwnerId) return Task.FromResult(PreconditionResult.FromSuccess());
18 | else return Task.FromResult(PreconditionResult.FromError(ErrorMessage));
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/CommandHandler.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 | using System.Reflection;
3 |
4 | namespace Discord_Stream_Notify_Bot.Command
5 | {
6 | class CommandHandler : ICommandService
7 | {
8 | private readonly DiscordSocketClient _client;
9 | private readonly CommandService _commands;
10 | private readonly IServiceProvider _services;
11 |
12 | public CommandHandler(IServiceProvider services, CommandService commands, DiscordSocketClient client)
13 | {
14 | _commands = commands;
15 | _services = services;
16 | _client = client;
17 | }
18 |
19 | public async Task InitializeAsync()
20 | {
21 | await _commands.AddModulesAsync(
22 | assembly: Assembly.GetEntryAssembly(),
23 | services: _services);
24 | _client.MessageReceived += (msg) => { var _ = Task.Run(() => HandleCommandAsync(msg)); return Task.CompletedTask; };
25 | }
26 |
27 | private async Task HandleCommandAsync(SocketMessage messageParam)
28 | {
29 | var message = messageParam as SocketUserMessage;
30 | if (message == null || message.Author.IsBot) return;
31 |
32 | int argPos = 0;
33 | if (message.HasStringPrefix("s!", ref argPos))
34 | {
35 | var context = new SocketCommandContext(_client, message);
36 |
37 | if (_commands.Search(context, argPos).IsSuccess)
38 | {
39 | var result = await _commands.ExecuteAsync(
40 | context: context,
41 | argPos: argPos,
42 | services: _services);
43 |
44 | if (!result.IsSuccess)
45 | {
46 | Log.FormatColorWrite($"[{context.Guild?.Name}/{context.Message.Channel?.Name}] {message.Author.Username} 執行 {context.Message} 發生錯誤", ConsoleColor.Red);
47 | Log.FormatColorWrite(result.ErrorReason, ConsoleColor.Red);
48 | await context.Channel.SendErrorAsync(result.ErrorReason);
49 | }
50 | else
51 | {
52 | try { if (context.Message.Author.Id == Bot.ApplicatonOwner.Id || context.Message.CleanContent == "s!ymlc") await message.DeleteAsync(); }
53 | catch { }
54 | Log.FormatColorWrite($"[{context.Guild?.Name}/{context.Message.Channel?.Name}] {message.Author.Username} 執行 {context.Message}", ConsoleColor.DarkYellow);
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/CommonEqualityComparer.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.Command
2 | {
3 | public class CommonEqualityComparer : IEqualityComparer
4 | {
5 | private Func keySelector;
6 |
7 | public CommonEqualityComparer(Func keySelector)
8 | {
9 | this.keySelector = keySelector;
10 | }
11 |
12 | public bool Equals(T x, T y)
13 | {
14 | return EqualityComparer.Default.Equals(keySelector(x), keySelector(y));
15 | }
16 |
17 | public int GetHashCode(T obj)
18 | {
19 | return EqualityComparer.Default.GetHashCode(keySelector(obj));
20 | }
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/Help/Help.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 |
3 | namespace Discord_Stream_Notify_Bot.Command.Help
4 | {
5 | public class Help : TopLevelModule
6 | {
7 | private readonly CommandService _cmds;
8 | private readonly IServiceProvider _services;
9 |
10 | public const string PatreonUrl = "https://patreon.com/jun112561";
11 | public const string PaypalUrl = "https://paypal.me/jun112561";
12 | public Help(CommandService cmds, IServiceProvider service)
13 | {
14 | _cmds = cmds;
15 | _services = service;
16 | }
17 |
18 | [Command("Modules")]
19 | [Summary("顯示模組")]
20 | public async Task Modules()
21 | {
22 | await ReplyAsync("", false, new EmbedBuilder().WithOkColor().WithTitle("模組清單")
23 | .WithDescription(string.Join("\n", _cmds.Modules.Select((x) => "。" + x.Name)))
24 | .WithFooter("輸入 `s!Commands 模組名稱` 以顯示模組內全部的指令,例 `s!Commands Help`")
25 | .Build());
26 | }
27 |
28 | [Command("Commands")]
29 | [Summary("顯示模組內包含的指令")]
30 | [Alias("Cmds")]
31 | public async Task Commands([Summary("模組名稱")] string module = null)
32 | {
33 | module = module?.Trim();
34 | if (string.IsNullOrWhiteSpace(module)) return;
35 |
36 | var cmds = _cmds.Commands.Where(c => c.Module.Name.ToUpperInvariant().StartsWith(module.ToUpperInvariant(), StringComparison.InvariantCulture)).OrderBy(c => c.Aliases[0]).Distinct(new CommandTextEqualityComparer());
37 | if (cmds.Count() == 0) { await Context.Channel.SendConfirmAsync($"找不到 {module} 模組"); return; }
38 |
39 | var succ = new HashSet((await Task.WhenAll(cmds.Select(async x =>
40 | {
41 | var pre = (await x.CheckPreconditionsAsync(Context, _services).ConfigureAwait(false));
42 | return (Cmd: x, Succ: pre.IsSuccess);
43 | })).ConfigureAwait(false))
44 | .Where(x => x.Succ)
45 | .Select(x => x.Cmd));
46 | cmds = cmds.Where(x => succ.Contains(x));
47 |
48 | if (cmds.Count() == 0)
49 | {
50 | await Context.Channel.SendConfirmAsync(module + " 未包含你可使用的指令");
51 | return;
52 | }
53 |
54 | var embed = new EmbedBuilder().WithOkColor().WithTitle($"{cmds.First().Module.Name} 內包含的指令").WithFooter("輸入 `s!Help 指令` 以顯示指令的詳細說明,例 `s!Help Help`");
55 | var commandList = new List();
56 |
57 | foreach (var item in cmds)
58 | {
59 | var prefix = "s!";
60 |
61 | var str = string.Format("**`{0}`**", prefix + item.Aliases.First());
62 | var alias = item.Aliases.Skip(1).FirstOrDefault();
63 | if (alias != null)
64 | str += string.Format(" **/ `{0}`**", prefix + alias);
65 |
66 | if (!commandList.Contains(str)) commandList.Add(str);
67 | }
68 | embed.WithDescription(string.Join('\n', commandList));
69 |
70 | await ReplyAsync("", false, embed.Build());
71 | }
72 |
73 | [Command("Help")]
74 | [Summary("顯示指令的詳細說明")]
75 | [Alias("H")]
76 | public async Task H([Summary("指令名稱")] string command = null)
77 | {
78 | command = command?.Trim();
79 |
80 | if (string.IsNullOrWhiteSpace(command))
81 | {
82 | EmbedBuilder embed = new EmbedBuilder().WithOkColor().WithFooter("輸入 `s!Modules` 取得所有的模組");
83 | embed.Title = "直播小幫手 建置版本" + Program.Version;
84 | #if DEBUG || DEBUG_DONTREGISTERCOMMAND
85 | embed.Title += " (測試版)";
86 | #endif
87 | embed.WithDescription(System.IO.File.ReadAllText(Utility.GetDataFilePath("HelpDescription.txt")).Replace("\\n", "\n") + $"\n\n您可以透過:\nPatreon <{PatreonUrl}> \nPaypal <{PaypalUrl}>\n來贊助直播小幫手");
88 | await ReplyAsync("", false, embed.Build());
89 | return;
90 | }
91 |
92 | CommandInfo commandInfo = _cmds.Commands.FirstOrDefault((x) => x.Aliases.Any((x2) => x2.ToLowerInvariant() == command.ToLowerInvariant()));
93 | if (commandInfo == null) { await Context.Channel.SendConfirmAsync($"找不到 {command} 指令"); return; }
94 |
95 | await ReplyAsync("", false, _service.GetCommandHelp(commandInfo).Build());
96 | }
97 | }
98 |
99 | public class CommandTextEqualityComparer : IEqualityComparer
100 | {
101 | public bool Equals(CommandInfo x, CommandInfo y) => x.Aliases[0] == y.Aliases[0];
102 |
103 | public int GetHashCode(CommandInfo obj) => obj.Aliases[0].GetHashCode(StringComparison.InvariantCulture);
104 |
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/Help/HelpService.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 | using Discord_Stream_Notify_Bot.Command.Attribute;
3 |
4 | namespace Discord_Stream_Notify_Bot.Command.Help
5 | {
6 | public class HelpService : ICommandService
7 | {
8 | public EmbedBuilder GetCommandHelp(CommandInfo com)
9 | {
10 | var prefix = "s!";
11 |
12 | var str = string.Format("**`{0}`**", prefix + com.Aliases.First());
13 | var alias = com.Aliases.Skip(1).FirstOrDefault();
14 | if (alias != null)
15 | str += string.Format(" **/ `{0}`**", prefix + alias);
16 | var em = new EmbedBuilder().WithTitle(com.Name)
17 | .AddField(fb => fb.WithName(str)
18 | .WithValue(com.Summary)
19 | .WithIsInline(true));
20 |
21 | if (com.Parameters.Count > 0)
22 | {
23 | string par = "";
24 | foreach (var item in com.Parameters)
25 | par += item.Name + " " + item.Summary + "\n";
26 | em.AddField("參數", par.TrimEnd('\n'));
27 | }
28 |
29 | var reqs = GetCommandRequirements(com);
30 | if (reqs.Any()) em.AddField("指令執行者權限要求", string.Join("\n", reqs));
31 |
32 | var botReqs = GetBotCommandRequirements(com);
33 | if (botReqs.Any()) em.AddField("Bot權限要求", string.Join("\n", botReqs));
34 |
35 | var exp = GetCommandExampleString(com);
36 | if (!string.IsNullOrEmpty(exp)) em.AddField("例子", exp);
37 |
38 | em.WithFooter(efb => efb.WithText("模組: " + com.Module.Name))
39 | .WithOkColor();
40 |
41 | return em;
42 | }
43 |
44 | public static string[] GetCommandRequirements(CommandInfo cmd) =>
45 | cmd.Preconditions
46 | .Where(ca => ca is RequireOwnerAttribute || ca is RequireUserPermissionAttribute)
47 | .Select(ca =>
48 | {
49 | if (ca is RequireOwnerAttribute)
50 | {
51 | return "Bot擁有者限定";
52 | }
53 |
54 | var cau = (RequireUserPermissionAttribute)ca;
55 | if (cau.GuildPermission != null)
56 | {
57 | return ("伺服器 " + cau.GuildPermission.ToString() + " 權限")
58 | .Replace("Guild", "Server", StringComparison.InvariantCulture);
59 | }
60 |
61 | return ("頻道 " + cau.ChannelPermission + " 權限")
62 | .Replace("Guild", "Server", StringComparison.InvariantCulture);
63 | })
64 | .ToArray();
65 |
66 | public static string[] GetBotCommandRequirements(CommandInfo cmd) =>
67 | cmd.Preconditions
68 | .Where(ca => ca is RequireBotPermissionAttribute)
69 | .Select(ca =>
70 | {
71 | var cau = (RequireBotPermissionAttribute)ca;
72 | if (cau.GuildPermission != null)
73 | {
74 | return ("伺服器 " + cau.GuildPermission.ToString() + " 權限")
75 | .Replace("Guild", "Server", StringComparison.InvariantCulture);
76 | }
77 |
78 | return ("頻道 " + cau.ChannelPermission + " 權限")
79 | .Replace("Guild", "Server", StringComparison.InvariantCulture);
80 | })
81 | .ToArray();
82 |
83 | public static string GetCommandExampleString(CommandInfo cmd)
84 | {
85 | var att = cmd.Attributes.FirstOrDefault((x) => x is CommandExampleAttribute);
86 | if (att == null) return "";
87 |
88 | var commandExampleAttribute = att as CommandExampleAttribute;
89 |
90 | return string.Join("\n", commandExampleAttribute.ExpArray
91 | .Select((x) => "`s!" + (cmd.Aliases.Count >= 2 ? cmd.Aliases.Last() : cmd.Name) + $" {x}`")
92 | .ToArray());
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/ICommandService.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.Command
2 | {
3 | public interface ICommandService
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/Normal/Normal.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 |
3 | namespace Discord_Stream_Notify_Bot.Command.Normal
4 | {
5 | public class Normal : TopLevelModule
6 | {
7 | private readonly DiscordSocketClient _client;
8 | private readonly HttpClients.DiscordWebhookClient _discordWebhookClient;
9 |
10 | public Normal(DiscordSocketClient client, HttpClients.DiscordWebhookClient discordWebhookClient)
11 | {
12 | _client = client;
13 | _discordWebhookClient = discordWebhookClient;
14 | }
15 |
16 | [Command("Ping")]
17 | [Summary("延遲檢測")]
18 | public async Task PingAsync()
19 | {
20 | await Context.Channel.SendConfirmAsync(":ping_pong: " + _client.Latency.ToString() + "ms");
21 | }
22 |
23 |
24 | [Command("Invite")]
25 | [Summary("取得邀請連結")]
26 | public async Task InviteAsync()
27 | {
28 | try
29 | {
30 | await (await Context.Message.Author.CreateDMChannelAsync())
31 | .SendConfirmAsync("");
32 | }
33 | catch (Exception) { await Context.Channel.SendErrorAsync("無法私訊,請確認已開啟伺服器內成員私訊許可"); }
34 | }
35 |
36 | [Command("Status")]
37 | [Summary("顯示機器人目前的狀態")]
38 | [Alias("Stats")]
39 | public async Task StatusAsync()
40 | {
41 | EmbedBuilder embedBuilder = new EmbedBuilder().WithOkColor();
42 | embedBuilder.WithTitle("直播小幫手");
43 | #if DEBUG || DEBUG_DONTREGISTERCOMMAND
44 | embedBuilder.Title += " (測試版)";
45 | #endif
46 |
47 | embedBuilder.WithDescription($"建置版本 {Program.Version}");
48 | embedBuilder.AddField("作者", "孤之界#1121", true);
49 | embedBuilder.AddField("擁有者", $"{Bot.ApplicatonOwner.Username}#{Bot.ApplicatonOwner.Discriminator}", true);
50 | embedBuilder.AddField("狀態", $"伺服器 {_client.Guilds.Count}\n服務成員數 {_client.Guilds.Sum((x) => x.MemberCount)}", false);
51 | embedBuilder.AddField("看過的直播數量", Utility.GetDbStreamCount(), true);
52 | embedBuilder.AddField("上線時間", $"{Bot.StopWatch.Elapsed:d\\天\\ hh\\:mm\\:ss}", false);
53 |
54 | await ReplyAsync(null, false, embedBuilder.Build());
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/ReactionEventWrapper.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.Command
2 | {
3 | public sealed class ReactionEventWrapper : IDisposable
4 | {
5 | public IUserMessage Message { get; }
6 | public event Action OnReactionAdded = delegate { };
7 | public event Action OnReactionRemoved = delegate { };
8 | public event Action OnReactionsCleared = delegate { };
9 |
10 | public ReactionEventWrapper(DiscordSocketClient client, IUserMessage msg)
11 | {
12 | Message = msg ?? throw new ArgumentNullException(nameof(msg));
13 | _client = client;
14 |
15 | _client.ReactionAdded += Discord_ReactionAdded;
16 | _client.ReactionRemoved += Discord_ReactionRemoved;
17 | _client.ReactionsCleared += Discord_ReactionsCleared;
18 | }
19 |
20 | private Task Discord_ReactionsCleared(Cacheable user, Cacheable channel)
21 | {
22 | Task.Run(async () =>
23 | {
24 | try
25 | {
26 | if ((await user.GetOrDownloadAsync()).Id == Message.Id)
27 | OnReactionsCleared?.Invoke();
28 | }
29 | catch { }
30 | });
31 |
32 | return Task.CompletedTask;
33 | }
34 |
35 | private Task Discord_ReactionRemoved(Cacheable user, Cacheable channel, SocketReaction reaction)
36 | {
37 | Task.Run(async () =>
38 | {
39 | try
40 | {
41 | if ((await user.GetOrDownloadAsync()).Id == Message.Id)
42 | OnReactionRemoved?.Invoke(reaction);
43 | }
44 | catch { }
45 | });
46 |
47 | return Task.CompletedTask;
48 | }
49 |
50 | private Task Discord_ReactionAdded(Cacheable user, Cacheable channel, SocketReaction reaction)
51 | {
52 | Task.Run(async () =>
53 | {
54 | try
55 | {
56 | if ((await user.GetOrDownloadAsync()).Id == Message.Id)
57 | OnReactionAdded?.Invoke(reaction);
58 | }
59 | catch { }
60 | });
61 |
62 | return Task.CompletedTask;
63 | }
64 |
65 | public void UnsubAll()
66 | {
67 | _client.ReactionAdded -= Discord_ReactionAdded;
68 | _client.ReactionRemoved -= Discord_ReactionRemoved;
69 | _client.ReactionsCleared -= Discord_ReactionsCleared;
70 | OnReactionAdded = null;
71 | OnReactionRemoved = null;
72 | OnReactionsCleared = null;
73 | }
74 |
75 | private bool disposing = false;
76 | private readonly DiscordSocketClient _client;
77 |
78 | public void Dispose()
79 | {
80 | if (disposing)
81 | return;
82 | disposing = true;
83 | UnsubAll();
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/TopLevelModule.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 |
3 | namespace Discord_Stream_Notify_Bot.Command
4 | {
5 | public abstract class TopLevelModule : ModuleBase
6 | {
7 | public async Task PromptUserConfirmAsync(EmbedBuilder embed)
8 | {
9 | embed.WithOkColor()
10 | .WithFooter("yes/no");
11 |
12 | var msg = await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
13 | try
14 | {
15 | var input = await GetUserInputAsync(Context.User.Id, Context.Channel.Id).ConfigureAwait(false);
16 | input = input?.ToUpperInvariant();
17 |
18 | if (input != "YES" && input != "Y")
19 | {
20 | return false;
21 | }
22 |
23 | return true;
24 | }
25 | finally
26 | {
27 | var _ = Task.Run(() => msg.DeleteAsync());
28 | }
29 | }
30 |
31 | public async Task GetUserInputAsync(ulong userId, ulong channelId)
32 | {
33 | var userInputTask = new TaskCompletionSource();
34 | var dsc = (DiscordSocketClient)Context.Client;
35 | try
36 | {
37 | dsc.MessageReceived += MessageReceived;
38 |
39 | if ((await Task.WhenAny(userInputTask.Task, Task.Delay(10000)).ConfigureAwait(false)) != userInputTask.Task)
40 | {
41 | return null;
42 | }
43 |
44 | return await userInputTask.Task.ConfigureAwait(false);
45 | }
46 | finally
47 | {
48 | dsc.MessageReceived -= MessageReceived;
49 | }
50 |
51 | Task MessageReceived(SocketMessage arg)
52 | {
53 | var _ = Task.Run(() =>
54 | {
55 | if (!(arg is SocketUserMessage userMsg) ||
56 | userMsg.Author.Id != userId ||
57 | userMsg.Channel.Id != channelId)
58 | {
59 | return Task.CompletedTask;
60 | }
61 |
62 | if (userInputTask.TrySetResult(arg.Content))
63 | {
64 | userMsg.DeleteAfter(1);
65 | }
66 | return Task.CompletedTask;
67 | });
68 | return Task.CompletedTask;
69 | }
70 | }
71 | }
72 |
73 | public abstract class TopLevelModule : TopLevelModule where TService : ICommandService
74 | {
75 | protected TopLevelModule()
76 | {
77 | }
78 |
79 | public TService _service { get; set; }
80 |
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/TwitCasting/TwitCasting.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 | using Discord_Stream_Notify_Bot.DataBase;
3 | using Discord_Stream_Notify_Bot.HttpClients;
4 |
5 | namespace Discord_Stream_Notify_Bot.Command.TwitCasting
6 | {
7 | public class TwitCasting : TopLevelModule
8 | {
9 | private readonly MainDbService _mainDbService;
10 | private readonly TwitcastingClient _twitcastingClient;
11 |
12 | public TwitCasting(MainDbService mainDbService, TwitcastingClient twitcastingClient)
13 | {
14 | _mainDbService = mainDbService;
15 | _twitcastingClient = twitcastingClient;
16 | }
17 |
18 | [RequireContext(ContextType.DM)]
19 | [Command("FixTCDb")]
20 | [Alias("ftcdb")]
21 | [RequireOwner]
22 | public async Task FixTCDbAsync()
23 | {
24 | await Context.Channel.TriggerTypingAsync().ConfigureAwait(false);
25 |
26 | using var db = _mainDbService.GetDbContext();
27 | var needFixList = db.TwitcastingSpider.Where((x) => string.IsNullOrEmpty(x.ChannelId)).ToList();
28 |
29 | foreach (var spider in needFixList)
30 | {
31 | var userInfo = await _twitcastingClient.GetUserInfoAsync(spider.ScreenId).ConfigureAwait(false);
32 | if (userInfo != null)
33 | {
34 | spider.ChannelId = userInfo.User.Id;
35 | }
36 | else
37 | {
38 | Log.Error($"Failed to fix TwitCasting Spider for ScreenId: {spider.ScreenId}");
39 | }
40 | }
41 |
42 | await db.SaveChangesAsync().ConfigureAwait(false);
43 |
44 | await Context.Channel.SendConfirmAsync("Fix TwitCasting Spider Database", $"Fixed {needFixList.Count} entries.").ConfigureAwait(false);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/Twitch/Twitch.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 | using Discord_Stream_Notify_Bot.Command.Attribute;
3 | using Discord_Stream_Notify_Bot.DataBase;
4 | using Discord_Stream_Notify_Bot.DataBase.Table;
5 |
6 | namespace Discord_Stream_Notify_Bot.Command.Twitch
7 | {
8 | public partial class Twitch : TopLevelModule, ICommandService
9 | {
10 | private readonly SharedService.Twitch.TwitchService _service;
11 | private readonly MainDbService _dbService;
12 |
13 | public Twitch(SharedService.Twitch.TwitchService service, MainDbService dbService)
14 | {
15 | _service = service;
16 | _dbService = dbService;
17 | }
18 |
19 | [RequireContext(ContextType.DM)]
20 | [RequireOwner]
21 | [Command("TwitchAddSpiderToGuild")]
22 | [Summary("新增爬蟲並指定伺服器")]
23 | [Alias("tastg")]
24 | public async Task AddSpiderToGuild(string channelUrl, ulong guildId)
25 | {
26 | string userLogin = _service.GetUserLoginByUrl(channelUrl);
27 |
28 | using (var db = _dbService.GetDbContext())
29 | {
30 | var twitchSpider = await db.TwitchSpider.AsNoTracking().SingleOrDefaultAsync((x) => x.UserLogin == userLogin);
31 | if (twitchSpider != null)
32 | {
33 | await Context.Channel.SendErrorAsync($"`{userLogin}` 已被 `{twitchSpider.GuildId}` 設定").ConfigureAwait(false);
34 | return;
35 | }
36 |
37 | var user = await _service.GetUserAsync(twitchUserLogin: userLogin);
38 | if (user == null)
39 | {
40 | await Context.Channel.SendErrorAsync($"頻道 `{userLogin}` 不存在").ConfigureAwait(false);
41 | return;
42 | }
43 |
44 | db.TwitchSpider.Add(new TwitchSpider() { UserId = user.Id, GuildId = guildId, UserLogin = user.Login, IsWarningUser = false, UserName = user.DisplayName });
45 | await db.SaveChangesAsync();
46 |
47 | await Context.Channel.SendConfirmAsync($"已將 `{user.DisplayName}` (`{user.Login}`) 設定至 `{guildId}`").ConfigureAwait(false);
48 | }
49 | }
50 |
51 | [RequireContext(ContextType.DM)]
52 | [RequireOwner]
53 | [Command("TwitchSetChannelSpiderGuildId")]
54 | [Summary("設定爬蟲頻道的伺服器 Id")]
55 | [CommandExample("https://twitch.tv/998rrr 0")]
56 | [Alias("tscsg")]
57 | public async Task SetChannelSpiderGuildId([Summary("頻道網址")] string channelUrl, ulong guildId = 0)
58 | {
59 | string userLogin = _service.GetUserLoginByUrl(channelUrl);
60 |
61 | using (var db = _dbService.GetDbContext())
62 | {
63 | var twitchSpider = await db.TwitchSpider.SingleOrDefaultAsync((x) => x.UserLogin == userLogin);
64 | if (twitchSpider != null)
65 | {
66 | twitchSpider.GuildId = guildId;
67 | db.TwitchSpider.Update(twitchSpider);
68 | await db.SaveChangesAsync();
69 |
70 | await Context.Channel.SendConfirmAsync($"已設定 `{twitchSpider.UserName}` (`{twitchSpider.UserLogin}`) 的 GuildId 為 `{guildId}`").ConfigureAwait(false);
71 | }
72 | else
73 | {
74 | await Context.Channel.SendErrorAsync($"尚未設定 `{userLogin}` 的爬蟲").ConfigureAwait(false);
75 | }
76 | }
77 | }
78 |
79 | [RequireContext(ContextType.DM)]
80 | [RequireOwner]
81 | [Command("TwitchToggleIsTrustedChannel")]
82 | [Summary("切換頻道是否為認可頻道")]
83 | [CommandExample("https://twitch.tv/998rrr")]
84 | [Alias("tttc")]
85 | public async Task ToggleIsTrustedChannel([Summary("頻道網址")] string channelUrl = "")
86 | {
87 | string userLogin = _service.GetUserLoginByUrl(channelUrl);
88 |
89 | using (var db = _dbService.GetDbContext())
90 | {
91 | var twitchSpider = await db.TwitchSpider.SingleOrDefaultAsync((x) => x.UserLogin == userLogin);
92 | if (twitchSpider != null)
93 | {
94 | twitchSpider.IsWarningUser = !twitchSpider.IsWarningUser;
95 | db.TwitchSpider.Update(twitchSpider);
96 | await db.SaveChangesAsync();
97 |
98 | await Context.Channel.SendConfirmAsync($"已設定 `{twitchSpider.UserName}` (`{twitchSpider.UserLogin}`) 為 __" + (twitchSpider.IsWarningUser ? "已" : "未") + "__ 認可頻道").ConfigureAwait(false);
99 | }
100 | else
101 | {
102 | await Context.Channel.SendErrorAsync($"尚未設定 `{userLogin}` 的爬蟲").ConfigureAwait(false);
103 | }
104 | }
105 | }
106 |
107 | [RequireContext(ContextType.DM)]
108 | [RequireOwner]
109 | [Command("TwitchRemoveChannelSpider")]
110 | [Summary("移除頻道爬蟲")]
111 | [CommandExample("https://twitch.tv/998rrr")]
112 | [Alias("trcs")]
113 | public async Task RemoveChannelSpider([Summary("頻道網址")] string channelUrl = "")
114 | {
115 | string userLogin = _service.GetUserLoginByUrl(channelUrl);
116 |
117 | using (var db = _dbService.GetDbContext())
118 | {
119 | var twitchSpider = await db.TwitchSpider.SingleOrDefaultAsync((x) => x.UserLogin == userLogin);
120 | if (twitchSpider != null)
121 | {
122 | db.TwitchSpider.Remove(twitchSpider);
123 | await db.SaveChangesAsync();
124 |
125 | await Context.Channel.SendConfirmAsync($"已移除 `{twitchSpider.UserName}` 的爬蟲").ConfigureAwait(false);
126 | }
127 | else
128 | {
129 | await Context.Channel.SendErrorAsync($"尚未設定 `{userLogin}` 的爬蟲").ConfigureAwait(false);
130 | }
131 | }
132 | }
133 |
134 | [RequireContext(ContextType.DM)]
135 | [RequireOwner]
136 | [Command("CreateEventSubSubscription")]
137 | [Summary("建立事件訂閱頻道")]
138 | [CommandExample("174268844")]
139 | [Alias("cess")]
140 | public async Task CreateEventSubSubscriptionAsync(string broadcasterUserId)
141 | {
142 | await Context.Channel.TriggerTypingAsync();
143 |
144 | if (await _service.CreateEventSubSubscriptionAsync(broadcasterUserId))
145 | {
146 | await Context.Channel.SendConfirmAsync($"已註冊圖奇事件通知: {broadcasterUserId}");
147 | }
148 | else
149 | {
150 | await Context.Channel.SendErrorAsync($"圖奇事件通知註冊失敗");
151 | }
152 | }
153 |
154 | [RequireContext(ContextType.DM)]
155 | [RequireOwner]
156 | [Command("TwitchGetLatestVOD")]
157 | [Summary("取得 Twitch 頻道最新的 VOD 資訊")]
158 | [CommandExample("https://twitch.tv/998rrr")]
159 | [Alias("tglv")]
160 | public async Task GetLatestVOD([Summary("頻道網址")] string channelUrl)
161 | {
162 | try
163 | {
164 | await Context.Channel.TriggerTypingAsync();
165 |
166 | string userLogin = _service.GetUserLoginByUrl(channelUrl);
167 |
168 | var user = await _service.GetUserAsync(twitchUserLogin: userLogin);
169 | if (user == null)
170 | {
171 | await Context.Channel.SendErrorAsync("找不到對應的 User 資料");
172 | return;
173 | }
174 |
175 | var video = await _service.GetLatestVODAsync(user.Id);
176 | if (video == null)
177 | {
178 | await Context.Channel.SendErrorAsync("找不到對應的 Video 資料");
179 | return;
180 | }
181 |
182 | var createAt = DateTime.Parse(video.CreatedAt);
183 | var endAt = createAt + _service.ParseToTimeSpan(video.Duration);
184 | var clips = await _service.GetClipsAsync(video.UserId, createAt, endAt);
185 |
186 | Log.Debug($"Duration: {video.Duration} | createAt: {createAt} | endAt: {endAt}");
187 |
188 | var embedBuilder = new EmbedBuilder()
189 | .WithOkColor()
190 | .WithTitle(video.Title)
191 | .WithUrl(video.Url)
192 | .WithDescription($"{Format.Url($"{video.UserName}", $"https://twitch.tv/{video.UserLogin}")}")
193 | .WithImageUrl(video.ThumbnailUrl.Replace("%{width}", "854").Replace("%{height}", "480"))
194 | .AddField("開始時間", createAt.ConvertDateTimeToDiscordMarkdown())
195 | .AddField("結束時間", endAt.ConvertDateTimeToDiscordMarkdown())
196 | .AddField("直播時長", video.Duration.Replace("h", "時").Replace("m", "分").Replace("s", "秒"));
197 |
198 | if (clips != null && clips.Any((x) => x.VideoId == video.Id))
199 | {
200 | Log.Debug(JsonConvert.SerializeObject(clips));
201 | int i = 0;
202 | embedBuilder.AddField("最多觀看的 Clip", string.Join('\n', clips.Where((x) => x.VideoId == video.Id)
203 | .Select((x) => $"{i++}. {Format.Url(x.Title, x.Url)} By `{x.CreatorName}` (`{x.ViewCount}` 次觀看)")));
204 | }
205 |
206 | await Context.Channel.SendMessageAsync(embed: embedBuilder.Build());
207 | }
208 | catch (Exception ex)
209 | {
210 | Log.Error(ex.Demystify(), "TwitchGetLatestVOD Error");
211 | await Context.Channel.SendErrorAsync("Error");
212 | }
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Command/YoutubeMember/YoutubeMember.cs:
--------------------------------------------------------------------------------
1 | using Discord.Commands;
2 | using Discord_Stream_Notify_Bot.DataBase;
3 |
4 | namespace Discord_Stream_Notify_Bot.Command.YoutubeMember
5 | {
6 | public class YoutubeMember : TopLevelModule, ICommandService
7 | {
8 | private readonly SharedService.Youtube.YoutubeStreamService _service;
9 | private readonly SharedService.YoutubeMember.YoutubeMemberService _ytMemberService;
10 | private readonly MainDbService _dbService;
11 |
12 | public YoutubeMember(SharedService.Youtube.YoutubeStreamService service, SharedService.YoutubeMember.YoutubeMemberService youtubeMemberService, MainDbService dbService)
13 | {
14 | _service = service;
15 | _ytMemberService = youtubeMemberService;
16 | _dbService = dbService;
17 | }
18 |
19 | [Command("ListAllGuildCheckedMember")]
20 | [Summary("顯示所有伺服器已完成驗證的會員數量")]
21 | [Alias("lagcm")]
22 | [RequireContext(ContextType.DM)]
23 | [RequireOwner]
24 | public async Task ListAllGuildCheckedMemberAsync(int page = 0)
25 | {
26 | using (var db = _dbService.GetDbContext())
27 | {
28 | var guildYoutubeMemberConfigs = db.GuildYoutubeMemberConfig
29 | .AsNoTracking()
30 | .Where((x) => !string.IsNullOrEmpty(x.MemberCheckChannelTitle) && x.MemberCheckVideoId != "-");
31 | if (!guildYoutubeMemberConfigs.Any())
32 | {
33 | await Context.Channel.SendErrorAsync($"清單為空");
34 | return;
35 | }
36 |
37 | var dic = new Dictionary>();
38 | foreach (var item in guildYoutubeMemberConfigs)
39 | {
40 | var checkedMemberCount = db.YoutubeMemberCheck.Count((x) => x.GuildId == item.GuildId &&
41 | x.CheckYTChannelId == item.MemberCheckChannelId && x.IsChecked);
42 |
43 | if (checkedMemberCount == 0)
44 | continue;
45 |
46 | string guildName = (await Context.Client.GetGuildAsync(item.GuildId)).Name;
47 | string formatStr = $"{Format.Url(item.MemberCheckChannelTitle, $"https://www.youtube.com/channel/{item.MemberCheckChannelId}")}: {checkedMemberCount}人";
48 |
49 | if (dic.TryGetValue(guildName, out List value)) value.Add(formatStr);
50 | else dic.Add(guildName, new List() { formatStr });
51 | }
52 |
53 | await Context.SendPaginatedConfirmAsync(page, (page) =>
54 | {
55 | return new EmbedBuilder().WithOkColor().WithDescription(string.Join('\n', dic
56 | .Skip(page * 7).Take(7).Select((x) =>
57 | $"**{x.Key}**:\n" +
58 | $"{string.Join('\n', x.Value)}\n"
59 | )));
60 | }, dic.Count(), 7);
61 | }
62 | }
63 |
64 | [Command("SetMemberCheckVideoId")]
65 | [Summary("設定指定頻道的會限影片Id")]
66 | [Alias("smcvi")]
67 | [RequireContext(ContextType.DM)]
68 | [RequireOwner]
69 | public async Task SetMemberCheckVideoIdAsync(string channelId, string videoId)
70 | {
71 | try
72 | {
73 | channelId = await _service.GetChannelIdAsync(channelId).ConfigureAwait(false);
74 | videoId = _service.GetVideoId(videoId);
75 | }
76 | catch (FormatException fex)
77 | {
78 | await Context.Channel.SendErrorAsync(fex.Message);
79 | return;
80 | }
81 | catch (ArgumentNullException)
82 | {
83 | await Context.Channel.SendErrorAsync("網址不可空白");
84 | return;
85 | }
86 |
87 | try
88 | {
89 | using (var db = _dbService.GetDbContext())
90 | {
91 | var guildYoutubeMemberConfigs = db.GuildYoutubeMemberConfig.Where((x) => x.MemberCheckChannelId == channelId);
92 | if (!guildYoutubeMemberConfigs.Any())
93 | {
94 | await Context.Channel.SendErrorAsync($"{channelId} 不存在資料");
95 | return;
96 | }
97 |
98 | foreach (var guildYoutubeMemberConfig in guildYoutubeMemberConfigs)
99 | {
100 | guildYoutubeMemberConfig.MemberCheckVideoId = videoId;
101 | db.GuildYoutubeMemberConfig.Update(guildYoutubeMemberConfig);
102 | }
103 |
104 | await db.SaveChangesAsync();
105 | await Context.Channel.SendConfirmAsync($"已將 `{guildYoutubeMemberConfigs.First().MemberCheckChannelTitle}` 的會限檢測影片更改為 `{guildYoutubeMemberConfigs.First().MemberCheckVideoId}`");
106 | }
107 | }
108 | catch (Exception ex)
109 | {
110 | Log.Error(ex.ToString());
111 | await Context.Channel.SendErrorAsync(ex.Message);
112 | }
113 | }
114 |
115 | [Command("StartNewMemberCheck")]
116 | [Summary("開始新會員的會限驗證")]
117 | [Alias("snmc")]
118 | [RequireContext(ContextType.DM)]
119 | [RequireOwner]
120 | public async Task StartNewMemberCheck()
121 | {
122 | await _ytMemberService.CheckMemberShip(false);
123 |
124 | await Context.Channel.SendConfirmAsync(":ok:");
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Data/HelpDescription.txt:
--------------------------------------------------------------------------------
1 | 由 孤之界(konnokai) 製作
2 |
3 | 可以幫你處理以下功能
4 | 1. YouTube 通知直播待機所建立/開台/關台(部分頻道)
5 | 2. YouTube 影片上傳通知(非主要功能)
6 | 3. YouTube 會限驗證並自動授予用戶組
7 | 4. TwitterSpace 開台通知
8 | 5. TwitCasting 開台通知
9 | 6. Twitch 開台通知
10 |
11 | __**其他特殊功能**__
12 | 部分頻道會有錄影,可以使用 `/youtube now-record-channel` 跟 `/youtube list-record-channel` 查詢
13 | 如果伺服器 Boost 達 Lv2 的話可以使用 `/youtube set-banner-change 頻道網址`,設定伺服器橫幅使用指定頻道的最新影片(直播)縮圖
14 | (可用 `/help get-command-help set-banner-change` 查看詳情)
15 |
16 | 邀請 Bot 後請開一個新的文字頻道做為直播通知用,並且開放 Bot 的讀取、發送訊息權限 (最好的做法是不要變更預設權限或直接給全權)
17 | 設定可至 [此網頁](https://konnokai.notion.site/a4fff40bd95c4bec9edca5b78cdd5d37) 查看詳細說明
18 |
19 | [點此邀請 Bot 到新伺服器](https://discordapp.com/api/oauth2/authorize?client_id=758222559392432160&permissions=2416143425&scope=bot%20applications.commands)
20 |
21 | 有新功能想加入或任何建議可以到推特私訊 [孤之界(@Jun112561)](https://twitter.com/Jun112561) 或到 [GitHub](https://github.com/konnokai/Discord-Stream-Notify-Bot) 發 Issue 討論
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/MainDbContext.cs:
--------------------------------------------------------------------------------
1 | using Discord_Stream_Notify_Bot.DataBase.Table;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase
4 | {
5 | public class MainDbContext : DbContext
6 | {
7 | private readonly string _connectionString;
8 |
9 | public MainDbContext(string connectionString
10 | // 要新增 Migration 的時候再把下面的連線字串註解拿掉
11 | //= "Server=localhost;Port=3306;User Id=stream_bot;Password=Ch@nge_Me;Database=discord_stream_bot"
12 | )
13 | {
14 | _connectionString = connectionString;
15 | }
16 |
17 | public DbSet BannerChange { get; set; }
18 | public DbSet GuildConfig { get; set; }
19 | public DbSet GuildYoutubeMemberConfig { get; set; }
20 | public DbSet NoticeTwitcastingStreamChannels { get; set; }
21 | public DbSet NoticeTwitchStreamChannels { get; set; }
22 | public DbSet NoticeTwitterSpaceChannel { get; set; }
23 | public DbSet NoticeYoutubeStreamChannel { get; set; }
24 | public DbSet RecordYoutubeChannel { get; set; }
25 | public DbSet TwitcastingSpider { get; set; }
26 | public DbSet TwitchSpider { get; set; }
27 | public DbSet TwitterSpace { get; set; }
28 | public DbSet TwitterSpaceSpider { get; set; }
29 | public DbSet YoutubeChannelNameToId { get; set; }
30 | public DbSet YoutubeChannelOwnedType { get; set; }
31 | public DbSet YoutubeChannelSpider { get; set; }
32 | public DbSet YoutubeMemberAccessToken { get; set; }
33 | public DbSet YoutubeMemberCheck { get; set; }
34 |
35 | #region Video
36 | public DbSet HoloVideos { get; set; }
37 | public DbSet NijisanjiVideos { get; set; }
38 | public DbSet OtherVideos { get; set; }
39 | public DbSet NonApprovedVideos { get; set; }
40 | public DbSet TwitcastingStreams { get; set; }
41 | public DbSet TwitchStreams { get; set; }
42 | #endregion
43 |
44 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
45 | {
46 | base.OnConfiguring(optionsBuilder);
47 | optionsBuilder
48 | .UseMySql(_connectionString, ServerVersion.AutoDetect(_connectionString))
49 | .UseSnakeCaseNamingConvention();
50 | }
51 |
52 | public bool UpdateAndSave(Table.Video video)
53 | {
54 | Table.Video updatedVideo = video switch
55 | {
56 | { ChannelType: Table.Video.YTChannelType.Holo } => video as HoloVideos,
57 | { ChannelType: Table.Video.YTChannelType.Nijisanji } => video as NijisanjiVideos,
58 | { ChannelType: Table.Video.YTChannelType.Other } => video as OtherVideos,
59 | { ChannelType: Table.Video.YTChannelType.NonApproved } => video as NonApprovedVideos,
60 | _ => null
61 | };
62 |
63 | if (updatedVideo == null)
64 | {
65 | return false;
66 | }
67 |
68 | Update(updatedVideo);
69 | var saveTime = DateTime.Now;
70 | bool saveFailed;
71 | int retryCount = 0;
72 | const int maxRetryCount = 5;
73 |
74 | do
75 | {
76 | saveFailed = false;
77 | try
78 | {
79 | SaveChanges();
80 | }
81 | catch (DbUpdateConcurrencyException ex)
82 | {
83 | saveFailed = true;
84 | retryCount++;
85 | foreach (var item in ex.Entries)
86 | {
87 | try
88 | {
89 | item.Reload();
90 | }
91 | catch (Exception ex2)
92 | {
93 | Log.Error($"VideoContext-SaveChanges-Reload");
94 | Log.Error(item.DebugView.ToString());
95 | Log.Error(ex2.ToString());
96 | }
97 | }
98 | }
99 | catch (Exception ex)
100 | {
101 | Log.Error($"VideoContext-SaveChanges: {ex}");
102 | Log.Error(ChangeTracker.DebugView.LongView);
103 | }
104 | } while (saveFailed && retryCount < maxRetryCount && DateTime.Now.Subtract(saveTime) <= TimeSpan.FromMinutes(1));
105 |
106 | return retryCount >= maxRetryCount || DateTime.Now.Subtract(saveTime) >= TimeSpan.FromMinutes(1);
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/MainDbService.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase
2 | {
3 | public class MainDbService
4 | {
5 | private readonly string _connectionString;
6 |
7 | public MainDbService(string connectionString)
8 | {
9 | _connectionString = connectionString;
10 | }
11 |
12 | public MainDbContext GetDbContext()
13 | {
14 | return new MainDbContext(_connectionString);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/BannerChange.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class BannerChange : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public string ChannelId { get; set; }
7 | public string LastChangeStreamId { get; set; } = null;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/DbEntity.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase.Table
4 | {
5 | public class DbEntity
6 | {
7 | [Key]
8 | public int Id { get; set; }
9 | public DateTime? DateAdded { get; set; } = DateTime.Now;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/GuildConfig.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class GuildConfig : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public ulong LogMemberStatusChannelId { get; set; } = 0;
7 | public ulong NoticeChannelId { get; set; } = 0;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/GuildYoutubeMemberConfig.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class GuildYoutubeMemberConfig : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public string MemberCheckChannelId { get; set; } = "";
7 | public string MemberCheckChannelTitle { get; set; } = "";
8 | public string MemberCheckVideoId { get; set; } = "-";
9 | public ulong MemberCheckGrantRoleId { get; set; } = 0;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/HoloVideo.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class HoloVideos : Video
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/NijisanjiVideo.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class NijisanjiVideos : Video
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/NonApprovedVideo.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class NonApprovedVideos : Video
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/NoticeTwitcastingStreamChannel.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class NoticeTwitcastingStreamChannel : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public ulong DiscordChannelId { get; set; }
7 | public string ScreenId { get; set; }
8 | public string StartStreamMessage { get; set; } = "";
9 | }
10 | }
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/NoticeTwitchStreamChannel.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class NoticeTwitchStreamChannel : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public ulong DiscordChannelId { get; set; }
7 | public string NoticeTwitchUserId { get; set; }
8 | public string StartStreamMessage { get; set; } = "";
9 | public string EndStreamMessage { get; set; } = "";
10 | public string ChangeStreamDataMessage { get; set; } = "";
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/NoticeTwitterSpaceChannel.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class NoticeTwitterSpaceChannel : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public ulong DiscordChannelId { get; set; }
7 | public string NoticeTwitterSpaceUserId { get; set; }
8 | public string NoticeTwitterSpaceUserScreenName { get; set; }
9 | public string StratTwitterSpaceMessage { get; set; } = "";
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/NoticeYoutubeStreamChannel.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class NoticeYoutubeStreamChannel : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public ulong DiscordNoticeVideoChannelId { get; set; }
7 | public ulong DiscordNoticeStreamChannelId { get; set; }
8 | public bool IsCreateEventForNewStream { get; set; } = false;
9 | public string YouTubeChannelId { get; set; }
10 | public string NewStreamMessage { get; set; } = "";
11 | public string NewVideoMessage { get; set; } = "";
12 | public string StratMessage { get; set; } = "";
13 | public string EndMessage { get; set; } = "";
14 | public string ChangeTimeMessage { get; set; } = "";
15 | public string DeleteMessage { get; set; } = "";
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/OtherVideo.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class OtherVideos : Video
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/RecordYoutubeChannel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase.Table
4 | {
5 | public class RecordYoutubeChannel
6 | {
7 | [Key]
8 | public string YoutubeChannelId { get; set; }
9 | public DateTime? DateAdded { get; set; } = DateTime.UtcNow;
10 | }
11 | }
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/TwitcastingSpider.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class TwitcastingSpider : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public string ChannelTitle { get; set; }
7 | public string ScreenId { get; set; }
8 | public string ChannelId { get; set; }
9 | public bool IsWarningUser { get; set; } = false;
10 | public bool IsRecord { get; set; } = false;
11 | }
12 | }
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/TwitcastingStream.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class TwitcastingStream : DbEntity
4 | {
5 | public string ChannelId { get; set; }
6 | public string ChannelTitle { get; set; }
7 | public int StreamId { get; set; }
8 | public string StreamTitle { get; set; } = "";
9 | public string StreamSubTitle { get; set; } = "";
10 | public string Category { get; set; } = "";
11 | public string ThumbnailUrl { get; set; } = "";
12 | public DateTime StreamStartAt { get; set; } = DateTime.Now;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/TwitchSpider.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase.Table
4 | {
5 | public class TwitchSpider
6 | {
7 | [Key]
8 | public string UserId { get; set; }
9 | public string UserLogin { get; set; }
10 | public string UserName { get; set; }
11 | public string ProfileImageUrl { get; set; } = "";
12 | public string OfflineImageUrl { get; set; } = "";
13 | public ulong GuildId { get; set; }
14 | public bool IsWarningUser { get; set; } = false;
15 | public bool IsRecord { get; set; } = false;
16 | public DateTime? DateAdded { get; set; } = DateTime.UtcNow;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/TwitchStream.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class TwitchStream : DbEntity
4 | {
5 | public string StreamId { get; set; }
6 | public string StreamTitle { get; set; }
7 | public DateTime StreamStartAt { get; set; }
8 | public string UserId { get; set; }
9 | public string UserLogin { get; set; }
10 | public string UserName { get; set; }
11 | public string GameName { get; set; } = "";
12 | public string ThumbnailUrl { get; set; } = "";
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/TwitterSpace.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class TwitterSpace : DbEntity
4 | {
5 | public string UserId { get; set; }
6 | public string UserScreenName { get; set; }
7 | public string UserName { get; set; }
8 | public string SpaecId { get; set; }
9 | public string SpaecTitle { get; set; }
10 | public DateTime SpaecActualStartTime { get; set; }
11 | public string SpaecMasterPlaylistUrl { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/TwitterSpaecSpider.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase.Table
4 | {
5 | public class TwitterSpaceSpider
6 | {
7 | [Key]
8 | public string UserId { get; set; }
9 | public string UserScreenName { get; set; } = null;
10 | public string UserName { get; set; } = null;
11 | public ulong GuildId { get; set; }
12 | public bool IsWarningUser { get; set; } = false;
13 | public bool IsRecord { get; set; } = true;
14 | public DateTime? DateAdded { get; set; } = DateTime.UtcNow;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/Video.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase.Table
4 | {
5 | public class Video
6 | {
7 | public enum YTChannelType
8 | {
9 | Holo, Nijisanji, Other, NonApproved
10 | }
11 |
12 | public string ChannelId { get; set; }
13 | public string ChannelTitle { get; set; }
14 | [Key]
15 | public string VideoId { get; set; }
16 | public string VideoTitle { get; set; }
17 | public DateTime ScheduledStartTime { get; set; }
18 | public YTChannelType ChannelType { get; set; }
19 | public bool IsPrivate { get; set; } = false;
20 |
21 | public override int GetHashCode()
22 | {
23 | return VideoId.ToCharArray().Sum((x) => x);
24 | }
25 |
26 | public override string ToString()
27 | {
28 | return ChannelTitle + " - " + VideoTitle;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/YoutubeChannelNameToId.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class YoutubeChannelNameToId : DbEntity
4 | {
5 | public string ChannelName { get; set; }
6 | public string ChannelId { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/YoutubeChannelOwnedType.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase.Table
4 | {
5 | public class YoutubeChannelOwnedType
6 | {
7 |
8 | [Key]
9 | public string ChannelId { get; set; }
10 | public string ChannelTitle { get; set; } = null;
11 | public Video.YTChannelType ChannelType { get; set; } = Video.YTChannelType.Other;
12 | public DateTime? DateAdded { get; set; } = DateTime.UtcNow;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/YoutubeChannelSpider.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase.Table
4 | {
5 | public class YoutubeChannelSpider
6 | {
7 | [Key]
8 | public string ChannelId { get; set; }
9 | public string ChannelTitle { get; set; } = null;
10 | public ulong GuildId { get; set; }
11 | public bool IsTrustedChannel { get; set; } = false;
12 | public DateTime LastSubscribeTime { get; set; } = DateTime.MinValue;
13 | public DateTime? DateAdded { get; set; } = DateTime.UtcNow;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/YoutubeMemberAccessToken.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Discord_Stream_Notify_Bot.DataBase.Table
4 | {
5 | public class YoutubeMemberAccessToken
6 | {
7 | [Key]
8 | public ulong DiscordUserId { get; set; }
9 | public string EncryptedAccessToken { get; set; }
10 | public DateTime? DateAdded { get; set; } = DateTime.Now;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/DataBase/Table/YoutubeMemberCheck.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.DataBase.Table
2 | {
3 | public class YoutubeMemberCheck : DbEntity
4 | {
5 | public ulong GuildId { get; set; }
6 | public ulong UserId { get; set; }
7 | public string CheckYTChannelId { get; set; }
8 | public DateTime LastCheckTime { get; set; } = DateTime.Now;
9 | public bool IsChecked { get; set; } = false;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/Discord Stream Notify Bot.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | Discord_Stream_Notify_Bot
7 | portable
8 | Debug;Release;Debug_DontRegisterCommand;Debug_API
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | all
28 | runtime; build; native; contentfiles; analyzers; buildtransitive
29 |
30 |
31 | all
32 | runtime; build; native; contentfiles; analyzers; buildtransitive
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | PreserveNewest
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | build$([System.DateTime]::UtcNow.AddHours(8).ToString("yyyy/MM/dd HH:mm:ss"))
63 | Logo_64.ico
64 | enable
65 |
66 |
67 |
68 | embedded
69 |
70 |
71 |
72 | embedded
73 |
74 |
75 |
76 | embedded
77 |
78 |
79 |
80 | embedded
81 |
82 |
83 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/HttpClients/DiscordWebhookClient.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace Discord_Stream_Notify_Bot.HttpClients
4 | {
5 | public class DiscordWebhookClient
6 | {
7 | private readonly HttpClient _httpClient;
8 | private readonly DiscordSocketClient _client;
9 | private readonly BotConfig _botConfig;
10 |
11 | public DiscordWebhookClient(HttpClient httpClient, DiscordSocketClient client, BotConfig botConfig)
12 | {
13 | httpClient.DefaultRequestHeaders.Add("UserAgent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36");
14 | _httpClient = httpClient;
15 | _client = client;
16 | _botConfig = botConfig;
17 | }
18 |
19 | public void SendMessageToDiscord(string content)
20 | {
21 | Message message = new Message();
22 |
23 | if (_client.CurrentUser != null)
24 | {
25 | message.username = _client.CurrentUser.Username;
26 | message.avatar_url = _client.CurrentUser.GetAvatarUrl();
27 | }
28 | else
29 | {
30 | message.username = "Bot";
31 | message.avatar_url = "";
32 | }
33 |
34 | message.content = content;
35 | var httpContent = new StringContent(JsonConvert.SerializeObject(message), Encoding.UTF8, "application/json");
36 | _httpClient.PostAsync(_botConfig.WebHookUrl, httpContent);
37 | }
38 |
39 | class Message
40 | {
41 | public string username { get; set; }
42 | public string content { get; set; }
43 | public string avatar_url { get; set; }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/HttpClients/TwitCasting/TwitcastingClient.cs:
--------------------------------------------------------------------------------
1 | using Discord_Stream_Notify_Bot.HttpClients.Twitcasting.Model;
2 | using System.Text;
3 |
4 | #nullable enable
5 |
6 | namespace Discord_Stream_Notify_Bot.HttpClients
7 | {
8 | public class TwitcastingClient
9 | {
10 | private readonly HttpClient _httpClient;
11 | private readonly HttpClient? _apiHttpClient;
12 |
13 | private readonly string? _twitcastingAccessToken;
14 | private static readonly string[] _events = ["livestart"];
15 |
16 | public TwitcastingClient(HttpClient httpClient, BotConfig botConfig)
17 | {
18 | _httpClient = httpClient;
19 |
20 | // https://apiv2-doc.twitcasting.tv/#access-token
21 | if (!string.IsNullOrEmpty(botConfig.TwitCastingClientId) && !string.IsNullOrEmpty(botConfig.TwitCastingClientSecret))
22 | {
23 | _twitcastingAccessToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{botConfig.TwitCastingClientId}:{botConfig.TwitCastingClientSecret}"));
24 |
25 | _apiHttpClient = new HttpClient();
26 | _apiHttpClient.BaseAddress = new Uri("https://apiv2.twitcasting.tv/");
27 | _apiHttpClient.DefaultRequestHeaders.Add("Accept", $"application/json");
28 | _apiHttpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {_twitcastingAccessToken}");
29 | _apiHttpClient.DefaultRequestHeaders.Add("X-Api-Version", $"2.0");
30 | }
31 | }
32 |
33 | ///
34 | /// 取得直播分類資料
35 | ///
36 | ///
37 | ///
38 | public async Task?> GetCategoriesAsync()
39 | {
40 | if (_apiHttpClient == null)
41 | throw new NullReferenceException(nameof(_apiHttpClient));
42 |
43 | try
44 | {
45 | var json = await _apiHttpClient.GetStringAsync($"categories?lang=ja");
46 | var data = JsonConvert.DeserializeObject(json);
47 | return data?.Categories;
48 | }
49 | catch (Exception ex)
50 | {
51 | Log.Error(ex.Demystify(), "TwitCastingClient.GetCategoriesAsync");
52 | return null;
53 | }
54 | }
55 |
56 | ///
57 | /// 取得頻道正在直播的資料
58 | ///
59 | ///
60 | /// 如果正在直播,則回傳 (, 影片 Id),否則為 (, 0)
61 | public async Task GetNewStreamDataAsync(string channelId)
62 | {
63 | try
64 | {
65 | var json = await _httpClient.GetStringAsync($"https://twitcasting.tv/streamserver.php?target={channelId}&mode=client");
66 | if (json == "{}")
67 | return null;
68 |
69 | var data = JsonConvert.DeserializeObject(json);
70 | return data;
71 | }
72 | catch (Exception ex)
73 | {
74 | Log.Error(ex.Demystify(), "TwitCastingClient.GetNewStreamDataAsync");
75 | return null;
76 | }
77 | }
78 |
79 | // https://apiv2-doc.twitcasting.tv/#get-movie-info
80 | ///
81 | /// 取得影片資料
82 | ///
83 | /// 影片 Id
84 | ///
85 | public async Task GetMovieInfoAsync(int movieId)
86 | {
87 | if (_apiHttpClient == null)
88 | throw new NullReferenceException(nameof(_apiHttpClient));
89 |
90 | if (movieId <= 0)
91 | throw new FormatException(nameof(movieId));
92 |
93 | try
94 | {
95 | var json = await _apiHttpClient.GetStringAsync($"movies/{movieId}");
96 | var data = JsonConvert.DeserializeObject(json);
97 | return data;
98 | }
99 | catch (Exception ex)
100 | {
101 | Log.Error(ex.Demystify(), "TwitCastingClient.GetMovieInfoAsync");
102 | return null;
103 | }
104 | }
105 |
106 | ///
107 | /// 取得使用者資訊
108 | ///
109 | /// 使用者 id 或 screen_id
110 | ///
111 | public async Task GetUserInfoAsync(string userIdOrScreenId)
112 | {
113 | if (_apiHttpClient == null)
114 | throw new NullReferenceException(nameof(_apiHttpClient));
115 |
116 | if (string.IsNullOrEmpty(userIdOrScreenId))
117 | throw new ArgumentNullException(nameof(userIdOrScreenId));
118 |
119 | try
120 | {
121 | var json = await _apiHttpClient.GetStringAsync($"users/{userIdOrScreenId}");
122 | var data = JsonConvert.DeserializeObject(json);
123 | return data;
124 | }
125 | catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
126 | {
127 | // Not Found
128 | return null;
129 | }
130 | catch (Exception ex)
131 | {
132 | Log.Error(ex.Demystify(), "TwitCastingClient.GetUserInfoAsync");
133 | return null;
134 | }
135 | }
136 |
137 | #region WebHook
138 | ///
139 | /// 取得所有已註冊的 WebHook
140 | ///
141 | /// 返回 WebHook 列表
142 | ///
143 | public async Task?> GetAllRegistedWebHookAsync()
144 | {
145 | if (_apiHttpClient == null)
146 | throw new NullReferenceException(nameof(_apiHttpClient));
147 |
148 | const int pageSize = 50;
149 | int offset = 0;
150 | int allCount = int.MaxValue;
151 | var result = new List();
152 |
153 | try
154 | {
155 | while (result.Count < allCount)
156 | {
157 | var url = $"webhooks?limit={pageSize}&offset={offset}";
158 | var jsonResponse = await _apiHttpClient.GetStringAsync(url);
159 | var data = JsonConvert.DeserializeObject(jsonResponse);
160 |
161 | if (data?.Webhooks == null || data.Webhooks.Count == 0)
162 | break;
163 |
164 | if (allCount == int.MaxValue)
165 | allCount = data.AllCount;
166 |
167 | result.AddRange(data.Webhooks);
168 | offset += pageSize;
169 | }
170 |
171 | return result;
172 | }
173 | catch (Exception ex)
174 | {
175 | Log.Error(ex.Demystify(), "TwitCastingClient.GetAllRegistedWebHookAsync");
176 | return null;
177 | }
178 | }
179 |
180 | ///
181 | /// 註冊 WebHook
182 | ///
183 | ///
184 | ///
185 | ///
186 | public async Task RegisterWebHookAsync(string channelId)
187 | {
188 | if (_apiHttpClient == null)
189 | throw new NullReferenceException(nameof(_apiHttpClient));
190 |
191 | if (string.IsNullOrEmpty(channelId))
192 | throw new NullReferenceException(nameof(channelId));
193 |
194 | try
195 | {
196 | var responseMessage = await _apiHttpClient.PostAsync("webhooks", new StringContent(JsonConvert.SerializeObject(new
197 | {
198 | user_id = channelId,
199 | events = _events
200 | })));
201 |
202 | responseMessage.EnsureSuccessStatusCode();
203 |
204 | return true;
205 | }
206 | catch (Exception ex)
207 | {
208 | Log.Error(ex.Demystify(), "TwitCastingClient.RegisterWebHookAsync");
209 | return null;
210 | }
211 | }
212 |
213 | ///
214 | /// 取消註冊 WebHook
215 | ///
216 | ///
217 | ///
218 | ///
219 | public async Task RemoveWebHookAsync(string channelId)
220 | {
221 | if (_apiHttpClient == null)
222 | throw new NullReferenceException(nameof(_apiHttpClient));
223 |
224 | if (string.IsNullOrEmpty(channelId))
225 | throw new NullReferenceException(nameof(channelId));
226 |
227 | try
228 | {
229 | var responseMessage = await _apiHttpClient.DeleteAsync($"webhooks?user_id={channelId}&events[]=livestart");
230 |
231 | responseMessage.EnsureSuccessStatusCode();
232 |
233 | return true;
234 | }
235 | catch (Exception ex)
236 | {
237 | Log.Error(ex.Demystify(), "TwitCastingClient.RemoveWebHookAsync");
238 | return null;
239 | }
240 | }
241 | #endregion
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/HttpClients/Twitcasting/Model/CategoriesJson.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.HttpClients.Twitcasting.Model
2 | {
3 | public class Category
4 | {
5 | [JsonProperty("id")]
6 | public string Id { get; set; }
7 |
8 | [JsonProperty("name")]
9 | public string Name { get; set; }
10 |
11 | [JsonProperty("sub_categories")]
12 | public List SubCategories { get; set; }
13 | }
14 |
15 | public class CategoriesJson
16 | {
17 | [JsonProperty("categories")]
18 | public List Categories { get; set; }
19 | }
20 |
21 | public class SubCategory
22 | {
23 | [JsonProperty("id")]
24 | public string Id { get; set; }
25 |
26 | [JsonProperty("name")]
27 | public string Name { get; set; }
28 |
29 | [JsonProperty("count")]
30 | public int Count { get; set; }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/HttpClients/Twitcasting/Model/GetAllRegistedWebHookJson.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.HttpClients.Twitcasting.Model
2 | {
3 | public class GetAllRegistedWebHookJson
4 | {
5 | [JsonProperty("all_count")]
6 | public int AllCount { get; set; }
7 |
8 | [JsonProperty("webhooks")]
9 | public List Webhooks { get; set; }
10 | }
11 |
12 | public class Webhook
13 | {
14 | [JsonProperty("user_id")]
15 | public string UserId { get; set; }
16 |
17 | [JsonProperty("event")]
18 | public string Event { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Discord Stream Notify Bot/HttpClients/Twitcasting/Model/GetMovieInfoResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Discord_Stream_Notify_Bot.HttpClients.Twitcasting.Model
2 | {
3 | public class Broadcaster
4 | {
5 | [JsonProperty("id")]
6 | public string Id { get; set; }
7 |
8 | [JsonProperty("screen_id")]
9 | public string ScreenId { get; set; }
10 |
11 | [JsonProperty("name")]
12 | public string Name { get; set; }
13 |
14 | [JsonProperty("image")]
15 | public string Image { get; set; }
16 |
17 | [JsonProperty("profile")]
18 | public string Profile { get; set; }
19 |
20 | [JsonProperty("level")]
21 | public int Level { get; set; }
22 |
23 | [JsonProperty("last_movie_id")]
24 | public string LastMovieId { get; set; }
25 |
26 | [JsonProperty("is_live")]
27 | public bool IsLive { get; set; }
28 |
29 | [JsonProperty("supporter_count")]
30 | public int SupporterCount { get; set; }
31 |
32 | [JsonProperty("supporting_count")]
33 | public int SupportingCount { get; set; }
34 |
35 | [JsonProperty("created")]
36 | public int Created { get; set; }
37 | }
38 |
39 | public class Movie
40 | {
41 | [JsonProperty("id")]
42 | public string Id { get; set; }
43 |
44 | [JsonProperty("user_id")]
45 | public string UserId { get; set; }
46 |
47 | [JsonProperty("title")]
48 | public string Title { get; set; }
49 |
50 | [JsonProperty("subtitle")]
51 | public string Subtitle { get; set; }
52 |
53 | [JsonProperty("last_owner_comment")]
54 | public object LastOwnerComment { get; set; }
55 |
56 | [JsonProperty("category")]
57 | public string Category { get; set; }
58 |
59 | [JsonProperty("link")]
60 | public string Link { get; set; }
61 |
62 | [JsonProperty("is_live")]
63 | public bool IsLive { get; set; }
64 |
65 | [JsonProperty("is_recorded")]
66 | public bool IsRecorded { get; set; }
67 |
68 | [JsonProperty("comment_count")]
69 | public int CommentCount { get; set; }
70 |
71 | [JsonProperty("large_thumbnail")]
72 | public string LargeThumbnail { get; set; }
73 |
74 | [JsonProperty("small_thumbnail")]
75 | public string SmallThumbnail { get; set; }
76 |
77 | [JsonProperty("country")]
78 | public string Country { get; set; }
79 |
80 | [JsonProperty("duration")]
81 | public int Duration { get; set; }
82 |
83 | [JsonProperty("created")]
84 | public int Created { get; set; }
85 |
86 | [JsonProperty("is_collabo")]
87 | public bool IsCollabo { get; set; }
88 |
89 | [JsonProperty("is_protected")]
90 | public bool IsProtected { get; set; }
91 |
92 | [JsonProperty("max_view_count")]
93 | public int MaxViewCount { get; set; }
94 |
95 | [JsonProperty("current_view_count")]
96 | public int CurrentViewCount { get; set; }
97 |
98 | [JsonProperty("total_view_count")]
99 | public int TotalViewCount { get; set; }
100 |
101 | [JsonProperty("hls_url")]
102 | public object HlsUrl { get; set; }
103 | }
104 |
105 | public class GetMovieInfoResponse
106 | {
107 | [JsonProperty("movie")]
108 | public Movie Movie { get; set; }
109 |
110 | [JsonProperty("broadcaster")]
111 | public Broadcaster Broadcaster { get; set; }
112 |
113 | [JsonProperty("tags")]
114 | public List