diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 1a6861331fc..4441924ba49 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -22,7 +22,7 @@ def edit; end def update if @user.update(user_params) destroy_subscription(@user) - Newspaper.publish(:retirement_create, @user) + Newspaper.publish(:retirement_create, @user) if @user.saved_change_to_retired_on? redirect_to admin_users_url, notice: 'ユーザー情報を更新しました。' else render :edit @@ -61,7 +61,7 @@ def user_params :auto_retire, :profile_image, :profile_name, :profile_job, :mentor, :profile_text, { authored_books_attributes: %i[id title url cover _destroy] }, - :country_code, :subdivision_code, discord_profile_attributes: %i[account_name times_url] + :country_code, :subdivision_code, discord_profile_attributes: %i[account_name times_url times_id] ) end diff --git a/app/models/discord/server.rb b/app/models/discord/server.rb index fabd2096047..0627d68b89e 100644 --- a/app/models/discord/server.rb +++ b/app/models/discord/server.rb @@ -17,6 +17,14 @@ def create_text_channel(name:, parent: nil) nil end + def delete_text_channel(channel_id) + response = Discordrb::API::Channel.delete(authorize_token, channel_id) + response.code == 200 + rescue Discordrb::Errors::CodeError => e + log_error(e) + nil + end + def find_by(id:, token:) return nil unless enabled? diff --git a/app/models/times_channel_destroyer.rb b/app/models/times_channel_destroyer.rb new file mode 100644 index 00000000000..9483690e50a --- /dev/null +++ b/app/models/times_channel_destroyer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class TimesChannelDestroyer + def call(user) + return unless user.discord_profile.times_id + + if Discord::Server.delete_text_channel(user.discord_profile.times_id) + user.discord_profile.update!(times_id: nil) + else + Rails.logger.warn "[Discord API] #{user.login_name}の分報チャンネルが削除できませんでした。" + end + end +end diff --git a/app/models/unfinished_data_destroyer.rb b/app/models/unfinished_data_destroyer.rb index 03500dc34cc..457ed121599 100644 --- a/app/models/unfinished_data_destroyer.rb +++ b/app/models/unfinished_data_destroyer.rb @@ -2,8 +2,6 @@ class UnfinishedDataDestroyer def call(user) - return unless user.saved_change_to_retired_on? - Product.where(user: user).unchecked.destroy_all Report.where(user: user).wip.destroy_all user.update(job_seeking: false) diff --git a/app/views/users/form/_sns.html.slim b/app/views/users/form/_sns.html.slim index 06e263f5fa9..62767add59d 100644 --- a/app/views/users/form/_sns.html.slim +++ b/app/views/users/form/_sns.html.slim @@ -26,6 +26,7 @@ | こちら span.a-help i.fa-solid.fa-question + = discord_profile_fields.hidden_field :times_id .form-item#form-github-account = f.label :github_account, class: 'a-form-label' diff --git a/config/initializers/newspaper.rb b/config/initializers/newspaper.rb index b145ba6d099..e350050dfbf 100644 --- a/config/initializers/newspaper.rb +++ b/config/initializers/newspaper.rb @@ -59,6 +59,7 @@ Newspaper.subscribe(:question_update, ai_answer_creator) Newspaper.subscribe(:retirement_create, UnfinishedDataDestroyer.new) + Newspaper.subscribe(:retirement_create, TimesChannelDestroyer.new) question_notifier = QuestionNotifier.new Newspaper.subscribe(:question_create, question_notifier) diff --git a/test/cassettes/discord/server/delete_text_channel.yml b/test/cassettes/discord/server/delete_text_channel.yml new file mode 100644 index 00000000000..953df3a176a --- /dev/null +++ b/test/cassettes/discord/server/delete_text_channel.yml @@ -0,0 +1,85 @@ +--- +http_interactions: + - request: + method: delete + uri: https://discord.com/api/v9/channels/987654321987654321 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - '*/*' + User-Agent: + - DiscordBot (https://github.com/shardlab/discordrb, v3.4.2) rest-client/2.1.0 + ruby/3.1.0p0 discordrb/3.4.2 + Authorization: + - Bot valid token + X-Audit-Log-Reason: + - '' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - discord.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Sat, 10 Jun 2023 10:07:20 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Set-Cookie: + - __cfruid=7bff7e41c3f5ddb14b24c420bb5796deb37ff70d-1686391640; path=/; domain=.discord.com; + HttpOnly; Secure; SameSite=None + - __dcfduid=966b708e077611eeaeaf2221179eb89d; Expires=Thu, 08-Jun-2028 10:07:20 + GMT; Max-Age=157680000; Secure; HttpOnly; Path=/ + - __sdcfduid=966b708e077611eeaeaf2221179eb89d5aa4609837dc30d830507bc86a70549d358973dcb44a755b3231cbac813b17d5; + Expires=Thu, 08-Jun-2028 10:07:20 GMT; Max-Age=157680000; Secure; HttpOnly; + Path=/ + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Ratelimit-Bucket: + - f7ead6a7674e5a323d93786263b66cb1 + X-Ratelimit-Limit: + - '50' + X-Ratelimit-Remaining: + - '49' + X-Ratelimit-Reset: + - '1686391640.427' + X-Ratelimit-Reset-After: + - '0.019' + Via: + - 1.1 google + Alt-Svc: + - h3=":443"; ma=86400 + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=fk1wUe90bYDOl0RetW1CjWpt%2BBPO0byhtnAICjfy3pZDX8uTPI%2Bj6rmVeytRI%2B%2BmzJbPbcWpQRPtIyy%2FoXndvgzgBP4IMxzp4vV%2Bp8caY2FqrxTdzl2rYa0ONrLh"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + X-Content-Type-Options: + - nosniff + Content-Security-Policy: + - frame-ancestors 'none'; default-src 'none' + Server: + - cloudflare + Cf-Ray: + - 7d50ce87bbd7341a-NRT + body: + encoding: UTF-8 + base64_string: | + ewogICJpZCI6ICIxMTE2OTE4MjQ3OTE3Mzc1NTA4IiwKICAidHlwZSI6IDAs + CiAgImxhc3RfbWVzc2FnZV9pZCI6IG51bGwsCiAgImZsYWdzIjogMCwKICAi + Z3VpbGRfaWQiOiAiMTEwNjg5NDAzNzMxMjYxMDM0NCIsCiAgIm5hbWUiOiAi + aHVuaG91c2FrdWpvMyIsCiAgInBhcmVudF9pZCI6ICIxMTE1MjQ3NTk0NzQ3 + MjIwMDIwIiwKICAicmF0ZV9saW1pdF9wZXJfdXNlciI6IDAsCiAgInRvcGlj + IjogbnVsbCwKICAicG9zaXRpb24iOiAxLAogICJwZXJtaXNzaW9uX292ZXJ3 + cml0ZXMiOiBbCgogIF0sCiAgIm5zZnciOiBmYWxzZQp9 + recorded_at: Sat, 10 Jun 2023 10:07:23 GMT +recorded_with: VCR 6.1.0 diff --git a/test/cassettes/discord/server/delete_text_channel_with_unauthorized.yml b/test/cassettes/discord/server/delete_text_channel_with_unauthorized.yml new file mode 100644 index 00000000000..1b86993606d --- /dev/null +++ b/test/cassettes/discord/server/delete_text_channel_with_unauthorized.yml @@ -0,0 +1,70 @@ +--- +http_interactions: +- request: + method: delete + uri: https://discord.com/api/v9/channels/987654321987654321 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*" + User-Agent: + - DiscordBot (https://github.com/shardlab/discordrb, v3.4.2) rest-client/2.1.0 + ruby/3.1.0p0 discordrb/3.4.2 + Authorization: + - Bot invalid token + X-Audit-Log-Reason: + - '' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - discord.com + response: + status: + code: 401 + message: Unauthorized + headers: + Date: + - Sat, 10 Jun 2023 10:19:23 GMT + Content-Type: + - application/json + Content-Length: + - '43' + Connection: + - keep-alive + Set-Cookie: + - __cfruid=bed5252b1b967c9e8921a418996483c9b7907488-1686392363; path=/; domain=.discord.com; + HttpOnly; Secure; SameSite=None + - __dcfduid=4560787c077811eeaa20ca361d91a146; Expires=Thu, 08-Jun-2028 10:19:23 + GMT; Max-Age=157680000; Secure; HttpOnly; Path=/ + - __sdcfduid=4560787c077811eeaa20ca361d91a1460d5df4f6d66a739fafcba67060b971d04930d04cdacd428b40ed829d88debc20; + Expires=Thu, 08-Jun-2028 10:19:23 GMT; Max-Age=157680000; Secure; HttpOnly; + Path=/ + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Via: + - 1.1 google + Alt-Svc: + - h3=":443"; ma=86400 + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=CQKHrJryjZa8FMjYdZB9D9jqg1X2cfM8VnQBQvSW7%2FwHi74%2BgaKZHZLUvzFZmnDBQV4O5e3wgfg2cU0uduF8glDppgnpfHOTYz59TotWlgY6wQHQlpgzVdh5t7rL"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + X-Content-Type-Options: + - nosniff + Content-Security-Policy: + - frame-ancestors 'none'; default-src 'none' + Server: + - cloudflare + Cf-Ray: + - 7d50e0301e3e0ac4-NRT + body: + encoding: UTF-8 + base64_string: | + ewogICJtZXNzYWdlIjogIjQwMTogVW5hdXRob3JpemVkIiwKICAiY29kZSI6 + IDAKfQ== + recorded_at: Sat, 10 Jun 2023 10:19:26 GMT +recorded_with: VCR 6.1.0 diff --git a/test/cassettes/discord/server/delete_text_channel_with_unknown_channel_id.yml b/test/cassettes/discord/server/delete_text_channel_with_unknown_channel_id.yml new file mode 100644 index 00000000000..e616d69b6b9 --- /dev/null +++ b/test/cassettes/discord/server/delete_text_channel_with_unknown_channel_id.yml @@ -0,0 +1,80 @@ +--- +http_interactions: + - request: + method: delete + uri: https://discord.com/api/v9/channels/12345 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - '*/*' + User-Agent: + - DiscordBot (https://github.com/shardlab/discordrb, v3.4.2) rest-client/2.1.0 + ruby/3.1.0p0 discordrb/3.4.2 + Authorization: + - Bot valid token + X-Audit-Log-Reason: + - '' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - discord.com + response: + status: + code: 404 + message: Not Found + headers: + Date: + - Sat, 10 Jun 2023 11:23:04 GMT + Content-Type: + - application/json + Content-Length: + - '45' + Connection: + - keep-alive + Set-Cookie: + - __cfruid=4a6c95ba041c6dcf13319831766efe977303c53c-1686396184; path=/; domain=.discord.com; + HttpOnly; Secure; SameSite=None + - __dcfduid=2aacf862078111eea466a671c7503be3; Expires=Thu, 08-Jun-2028 11:23:04 + GMT; Max-Age=157680000; Secure; HttpOnly; Path=/ + - __sdcfduid=2aacf862078111eea466a671c7503be39177ef6fd6863822b5cd7f90ee04d5e099467abc3838f544cb8aee5841514b21; + Expires=Thu, 08-Jun-2028 11:23:04 GMT; Max-Age=157680000; Secure; HttpOnly; + Path=/ + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Ratelimit-Bucket: + - f7ead6a7674e5a323d93786263b66cb1 + X-Ratelimit-Limit: + - '50' + X-Ratelimit-Remaining: + - '49' + X-Ratelimit-Reset: + - '1686396184.335' + X-Ratelimit-Reset-After: + - '0.019' + Via: + - 1.1 google + Alt-Svc: + - h3=":443"; ma=86400 + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=koWsVqysLyFanFJwyICNYiaTKKk6LklX0%2FlA8Hc1ySsRk2i3YSl5qESSha%2ByGhOikSqOJyTM4%2FmNaObQINcrZ%2F3t2cdLXqK8Z15BZ5%2Bvg7%2FbHwWA52B9hBMNMQZB"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + X-Content-Type-Options: + - nosniff + Content-Security-Policy: + - frame-ancestors 'none'; default-src 'none' + Server: + - cloudflare + Cf-Ray: + - 7d513d773e18f655-NRT + body: + encoding: UTF-8 + base64_string: | + ewogICJtZXNzYWdlIjogIlVua25vd24gQ2hhbm5lbCIsCiAgImNvZGUiOiAx + MDAwMwp9 + recorded_at: Sat, 10 Jun 2023 11:23:06 GMT +recorded_with: VCR 6.1.0 diff --git a/test/models/discord/server_test.rb b/test/models/discord/server_test.rb index 67e3825ae4e..87ff1763e5b 100644 --- a/test/models/discord/server_test.rb +++ b/test/models/discord/server_test.rb @@ -96,6 +96,28 @@ class ServerTest < ActiveSupport::TestCase end end + test '.delete_text_channel' do + VCR.use_cassette 'discord/server/delete_text_channel' do + assert Discord::Server.delete_text_channel('987654321987654321') + end + end + + test '.delete_text_channel with error' do + logs = [] + Rails.logger.stub(:error, ->(message) { logs << message }) do + VCR.use_cassette 'discord/server/delete_text_channel_with_unknown_channel_id' do + assert_nil Discord::Server.delete_text_channel('12345') + assert_equal '[Discord API] Unknown Channel', logs.pop + end + + VCR.use_cassette 'discord/server/delete_text_channel_with_unauthorized' do + Discord::Server.authorize_token = 'Bot invalid token' + assert_nil Discord::Server.delete_text_channel('987654321987654321') + assert_equal '[Discord API] 401: Unauthorized', logs.pop + end + end + end + test '.enabled?' do Discord::Server.guild_id = '1234567890123456789' Discord::Server.authorize_token = 'Bot valid token' diff --git a/test/models/times_channel_destroyer_test.rb b/test/models/times_channel_destroyer_test.rb new file mode 100644 index 00000000000..edda241daa7 --- /dev/null +++ b/test/models/times_channel_destroyer_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'test_helper' + +class TimesChannelCreatorTest < ActiveSupport::TestCase + test '#call' do + logs = [] + user = users(:hajime) + user.discord_profile.update!(times_id: '987654321987654321') + Rails.logger.stub(:warn, ->(message) { logs << message }) do + Discord::Server.stub(:delete_text_channel, true) do + TimesChannelDestroyer.new.call(user) + end + assert_nil user.discord_profile.times_id + assert_nil logs.last + end + end + + test '#call with failure' do + logs = [] + user = users(:hajime) + user.discord_profile.update!(times_id: '987654321987654321') + Rails.logger.stub(:warn, ->(message) { logs << message }) do + Discord::Server.stub(:delete_text_channel, nil) do + TimesChannelDestroyer.new.call(user) + end + assert_equal '987654321987654321', user.discord_profile.times_id + assert_equal "[Discord API] #{user.login_name}の分報チャンネルが削除できませんでした。", logs.last + end + end +end diff --git a/test/system/admin/users_test.rb b/test/system/admin/users_test.rb index 3aa5493f326..d22986feced 100644 --- a/test/system/admin/users_test.rb +++ b/test/system/admin/users_test.rb @@ -132,15 +132,19 @@ class Admin::UsersTest < ApplicationSystemTestCase test 'make user retired' do user = users(:hatsuno) + user.discord_profile.update!(times_id: '987654321987654321') date = Date.current - VCR.use_cassette 'subscription/update' do - visit_with_auth edit_admin_user_path(user.id), 'komagata' - check '退会済', allow_label_click: true - fill_in 'user_retired_on', with: date - click_on '更新する' + Discord::Server.stub(:delete_text_channel, true) do + VCR.use_cassette 'subscription/update' do + visit_with_auth edit_admin_user_path(user.id), 'komagata' + check '退会済', allow_label_click: true + fill_in 'user_retired_on', with: date + click_on '更新する' + end end assert_text 'ユーザー情報を更新しました。' assert_equal date, user.reload.retired_on + assert_nil user.discord_profile.times_id assert_requested(:post, "https://api.stripe.com/v1/subscriptions/#{user.subscription_id}") do |req| req.body.include?('cancel_at_period_end=true') diff --git a/test/system/auto_retire_test.rb b/test/system/auto_retire_test.rb index 77d73b9da82..9d97ebe3d77 100644 --- a/test/system/auto_retire_test.rb +++ b/test/system/auto_retire_test.rb @@ -99,6 +99,21 @@ class AutoRetireTest < ApplicationSystemTestCase assert_equal 0, user.reports.wip.count end + test 'delete times channel when retire' do + user = users(:kyuukai) + user.discord_profile.update!(times_id: '987654321987654321') + + travel_to Time.zone.local(2020, 7, 2, 0, 0, 0) do + Discord::Server.stub(:delete_text_channel, true) do + VCR.use_cassette 'subscription/update' do + visit_with_auth scheduler_daily_auto_retire_path, 'komagata' + end + end + assert_equal Date.current, user.reload.retired_on + end + assert_nil user.discord_profile.times_id + end + test 'retire with postmark error' do user = users(:kyuukai) logs = [] diff --git a/test/system/retirement_test.rb b/test/system/retirement_test.rb index 107ecd9f397..e8b2376cd3b 100644 --- a/test/system/retirement_test.rb +++ b/test/system/retirement_test.rb @@ -40,6 +40,20 @@ class RetirementTest < ApplicationSystemTestCase assert_text '退会したユーザーです' end + test 'retire user with times_channel' do + user = users(:hajime) + user.discord_profile.update!(times_id: '987654321987654321') + Discord::Server.stub(:delete_text_channel, true) do + visit_with_auth new_retirement_path, user.login_name + find('label', text: 'とても良い').click + click_on '退会する' + page.driver.browser.switch_to.alert.accept + assert_text '退会処理が完了しました' + end + assert_equal Date.current, user.reload.retired_on + assert_nil user.discord_profile.times_id + end + test 'retire user with postmark error' do logs = [] stub_warn_logger = ->(message) { logs << message }