diff --git a/Gemfile b/Gemfile index fa6b8f76f..28f52f5cd 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,7 @@ group :development do gem 'web-console' gem 'spring' gem 'listen' + gem 'dotenv-rails' end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 6e37112db..adf5064d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,6 +76,10 @@ GEM declarative (0.0.10) declarative-option (0.1.0) diff-lcs (1.3) + dotenv (2.5.0) + dotenv-rails (2.5.0) + dotenv (= 2.5.0) + railties (>= 3.2, < 6.0) erubi (1.7.1) erubis (2.7.0) ethon (0.11.0) @@ -385,6 +389,7 @@ DEPENDENCIES bootstrap-sass capybara coffee-rails + dotenv-rails faraday faraday_middleware (= 0.10) font-awesome-rails @@ -426,4 +431,4 @@ RUBY VERSION ruby 2.4.2p198 BUNDLED WITH - 1.16.1 + 1.16.3 diff --git a/app/models/dojo_event_service.rb b/app/models/dojo_event_service.rb index bcde8ceab..7e3372e33 100644 --- a/app/models/dojo_event_service.rb +++ b/app/models/dojo_event_service.rb @@ -2,6 +2,8 @@ class DojoEventService < ApplicationRecord EXTERNAL_SERVICES = %i( connpass doorkeeper facebook ) INTERNAL_SERVICES = %i( static_yaml ) + has_many :upcoming_events + belongs_to :dojo enum name: EXTERNAL_SERVICES + INTERNAL_SERVICES diff --git a/app/models/upcoming_event.rb b/app/models/upcoming_event.rb new file mode 100644 index 000000000..d0112b39a --- /dev/null +++ b/app/models/upcoming_event.rb @@ -0,0 +1,12 @@ +class UpcomingEvent < ApplicationRecord + belongs_to :dojo + belongs_to :dojo_event_service + + validates :dojo_event_service_id, presence: true + validates :event_id, presence: true + validates :event_url,presence: true + + validates :event_at,presence: true + + scope :for, ->(service) { UpcomingEvent.where(dojo_event_service: DojoEventService.for(service)) } +end \ No newline at end of file diff --git a/lib/tasks/upcoming.rake b/lib/tasks/upcoming.rake new file mode 100644 index 000000000..fad877abb --- /dev/null +++ b/lib/tasks/upcoming.rake @@ -0,0 +1,9 @@ +require_relative '../upcoming.rb' + +namespace :upcoming do + desc '月次のイベント開催予定を集計します' + task :aggregation, [:from, :to] => :environment do |tasks, args| + EventService::Providers::Facebook.access_token = Koala::Facebook::OAuth.new(ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET']).get_app_access_token + Upcoming::Aggregation.new(args).run + end +end diff --git a/lib/upcoming.rb b/lib/upcoming.rb new file mode 100644 index 000000000..a411e7384 --- /dev/null +++ b/lib/upcoming.rb @@ -0,0 +1,5 @@ +module Upcoming; end + +require_relative 'upcoming/tasks' +require_relative 'upcoming/aggregation' +require_relative 'event_service' diff --git a/lib/upcoming/aggregation.rb b/lib/upcoming/aggregation.rb new file mode 100644 index 000000000..f47b5dc18 --- /dev/null +++ b/lib/upcoming/aggregation.rb @@ -0,0 +1,133 @@ +module Upcoming + class Aggregation + def initialize(args) + @from = Time.current.beginning_of_week + @to = Time.current.next_month + @dojos = fetch_dojos + end + + def run + Upcoming::Aggregation::Monthly.new(@dojos, @from, @to).run + end + + private + + def fetch_dojos + { + externals: find_dojos_by(DojoEventService::EXTERNAL_SERVICES), + internals: find_dojos_by(DojoEventService::INTERNAL_SERVICES) + } + end + + def find_dojos_by(services) + services.each.with_object({}) do |name, hash| + hash[name] = Dojo.eager_load(:dojo_event_services).where(dojo_event_services: { name: name }).to_a + end + end + + class Base + def initialize(dojos, from, to) + @externals = dojos[:externals] + @internals = dojos[:internals] + @list = build_list(from, to) + @from = from + @to = to + end + + def run + with_notifying do + delete_upcoming_event + execute + execute_once + end + end + + private + + def with_notifying + yield + Notifier.notify_success(date_format(@from), date_format(@to)) + rescue => e + Notifier.notify_failure(date_format(@from), date_format(@to), e) + end + + def delete_upcoming_event + (@externals.keys + @internals.keys).each do |kind| + "Upcoming::Tasks::#{kind.to_s.camelize}".constantize.delete_upcoming_event + end + end + + def execute + raise NotImplementedError.new("You must implement #{self.class}##{__method__}") + end + + def execute_once + @internals.each do |kind, list| + "Upcoming::Tasks::#{kind.to_s.camelize}".constantize.new(list, nil).run + end + end + + def build_list(_from, _to) + raise NotImplementedError.new("You must implement #{self.class}##{__method__}") + end + + def date_format(_date) + raise NotImplementedError.new("You must implement #{self.class}##{__method__}") + end + end + + class Monthly < Base + private + + def execute + @list.each do |date| + puts "Aggregate for #{date_format(date)}" + + @externals.each do |kind, list| + "Upcoming::Tasks::#{kind.to_s.camelize}".constantize.new(list, date).run + end + end + end + + def build_list(from, to) + loop.with_object([from]) do |_, list| + nm = list.last.next_month + raise StopIteration if nm > to + list << nm + end + end + + def date_format(date) + date.strftime('%Y/%m') + end + end + + class Notifier + class << self + def notify_success(from, to) + notify("#{from}~#{to}のイベント登録を行いました") + end + + def notify_failure(from, to, exception) + notify("#{from}~#{to}のイベント登録でエラーが発生しました\n#{exception.message}\n#{exception.backtrace.join("\n")}") + end + + private + + def idobata_hook_url + return @idobata_hook_url if defined?(@idobata_hook_url) + @idobata_hook_url = ENV['IDOBATA_HOOK_URL'] + end + + def notifierable? + idobata_hook_url.present? + end + + def notify(msg) + $stdout.puts msg + puts `curl --data-urlencode "source=#{msg}" -s #{idobata_hook_url} -o /dev/null -w "idobata: %{http_code}"` if notifierable? + end + end + end + end +end diff --git a/lib/upcoming/tasks.rb b/lib/upcoming/tasks.rb new file mode 100644 index 000000000..3ae84daaa --- /dev/null +++ b/lib/upcoming/tasks.rb @@ -0,0 +1,9 @@ +module Upcoming + module Tasks + end +end + +require_relative 'tasks/connpass' +require_relative 'tasks/doorkeeper' +require_relative 'tasks/facebook' +require_relative 'tasks/static_yaml' diff --git a/lib/upcoming/tasks/connpass.rb b/lib/upcoming/tasks/connpass.rb new file mode 100644 index 000000000..d6c90c384 --- /dev/null +++ b/lib/upcoming/tasks/connpass.rb @@ -0,0 +1,36 @@ +module Upcoming + module Tasks + class Connpass + def self.delete_upcoming_event + UpcomingEvent.for(:connpass).delete_all + end + + def initialize(dojos, date) + @client = EventService::Providers::Connpass.new + @dojos = dojos + @params = build_params(date) + end + + def run + @dojos.each do |dojo| + dojo.dojo_event_services.for(:connpass).each do |dojo_event_service| + @client.fetch_events(@params.merge(series_id: dojo_event_service.group_id)).each do |e| + next unless e.dig('series', 'id').to_s == dojo_event_service.group_id + + UpcomingEvent.create!(dojo_event_service: dojo_event_service, + event_id: e['event_id'], + event_url: e['event_url'], + event_at: Time.zone.parse(e['started_at'])) + end + end + end + end + + private + + def build_params(date) + { yyyymm: "#{date.year}#{date.month}" } + end + end + end +end diff --git a/lib/upcoming/tasks/doorkeeper.rb b/lib/upcoming/tasks/doorkeeper.rb new file mode 100644 index 000000000..047dd7ac5 --- /dev/null +++ b/lib/upcoming/tasks/doorkeeper.rb @@ -0,0 +1,39 @@ +module Upcoming + module Tasks + class Doorkeeper + def self.delete_upcoming_event + UpcomingEvent.for(:doorkeeper).delete_all + end + + def initialize(dojos, date) + @client = EventService::Providers::Doorkeeper.new + @dojos = dojos + @params = build_params(date) + end + + def run + @dojos.each do |dojo| + dojo.dojo_event_services.for(:doorkeeper).each do |dojo_event_service| + @client.fetch_events(@params.merge(group_id: dojo_event_service.group_id)).each do |e| + next unless e['group'].to_s == dojo_event_service.group_id + + UpcomingEvent.create!(dojo_event_service: dojo_event_service, + event_id: e['id'], + event_url: e['public_url'], + event_at: Time.zone.parse(e['starts_at'])) + end + end + end + end + + private + + def build_params(date) + { + since_at: date.beginning_of_month, + until_at: date.end_of_month + } + end + end + end +end diff --git a/lib/upcoming/tasks/facebook.rb b/lib/upcoming/tasks/facebook.rb new file mode 100644 index 000000000..5251f77fe --- /dev/null +++ b/lib/upcoming/tasks/facebook.rb @@ -0,0 +1,39 @@ +module Upcoming + module Tasks + class Facebook + def self.delete_upcoming_event + UpcomingEvent.for(:facebook).delete_all + end + + def initialize(dojos, date) + @client = EventService::Providers::Facebook.new + @dojos = dojos + @params = build_params(date) + end + + def run + @dojos.each do |dojo| + dojo.dojo_event_services.for(:facebook).each do |dojo_event_service| + @client.fetch_events(@params.merge(group_id: dojo_event_service.group_id)).each do |e| + next unless e.dig('owner', 'id') == dojo_event_service.group_id + + UpcomingEvent.create!(dojo_event_service_id: e['id'], + event_id: e['id'], + event_url: "https://www.facebook.com/events/#{e['id']}", + event_at: Time.zone.parse(e['start_time'])) + end + end + end + end + + private + + def build_params(date) + { + since_at: date.beginning_of_month, + until_at: date.end_of_month + } + end + end + end +end diff --git a/lib/upcoming/tasks/static_yaml.rb b/lib/upcoming/tasks/static_yaml.rb new file mode 100644 index 000000000..f8350e03c --- /dev/null +++ b/lib/upcoming/tasks/static_yaml.rb @@ -0,0 +1,33 @@ +module Upcoming + module Tasks + class StaticYaml + def self.delete_upcoming_event + UpcomingEvent.for(:static_yaml).delete_all + end + + def initialize(dojos, _date) + @client = EventService::Providers::StaticYaml.new + @dojos = dojos + end + + def run + dojos_hash = @dojos.index_by(&:id) + @client.fetch_events.each do |e| + dojo = dojos_hash[e['dojo_id'].to_i] + next unless dojo + + dojo.dojo_event_services.for(:static_yaml).each do |dojo_event_service| + evented_at = Time.zone.parse(e['evented_at']) + event_id = "#{SecureRandom.uuid}" + + UpcomingEvent.create!(dojo_event_service: dojo_event_service, + event_id: event_id, + event_url: "https://dummy.url/#{event_id}", + event_at: evented_at) + + end + end + end + end + end +end diff --git a/logfile b/logfile new file mode 100644 index 000000000..d24ba0432 --- /dev/null +++ b/logfile @@ -0,0 +1,24 @@ +2018-06-26 17:30:30.851 JST [91625] LOG: listening on IPv6 address "::1", port 5432 +2018-06-26 17:30:30.851 JST [91625] LOG: listening on IPv4 address "127.0.0.1", port 5432 +2018-06-26 17:30:30.852 JST [91625] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432" +2018-06-26 17:30:30.978 JST [91626] LOG: database system was shut down at 2018-06-13 16:41:22 JST +2018-06-26 17:30:30.998 JST [91625] LOG: database system is ready to accept connections +2018-06-26 17:30:53.572 JST [91837] ERROR: column upcoming_events.service_name does not exist at character 37 +2018-06-26 17:30:53.572 JST [91837] STATEMENT: DELETE FROM "upcoming_events" WHERE "upcoming_events"."service_name" = $1 +2018-06-26 17:31:34.879 JST [92027] ERROR: database "coderdojo_jp_development" already exists +2018-06-26 17:31:34.879 JST [92027] STATEMENT: CREATE DATABASE "coderdojo_jp_development" ENCODING = 'unicode' +2018-06-26 17:31:34.890 JST [92028] ERROR: database "coderdojo_jp_test" already exists +2018-06-26 17:31:34.890 JST [92028] STATEMENT: CREATE DATABASE "coderdojo_jp_test" ENCODING = 'unicode' +2018-06-26 17:32:08.025 JST [92199] ERROR: column upcoming_events.service_name does not exist at character 37 +2018-06-26 17:32:08.025 JST [92199] STATEMENT: DELETE FROM "upcoming_events" WHERE "upcoming_events"."service_name" = $1 +2018-06-28 14:54:47.903 JST [91625] LOG: received smart shutdown request +2018-06-28 14:54:47.906 JST [91625] LOG: worker process: logical replication launcher (PID 91632) exited with exit code 1 +2018-06-28 14:54:47.906 JST [91627] LOG: shutting down +2018-06-28 14:54:47.925 JST [91625] LOG: database system is shut down +2018-07-23 21:50:52.263 JST [90475] LOG: listening on IPv6 address "::1", port 5432 +2018-07-23 21:50:52.265 JST [90475] LOG: listening on IPv4 address "127.0.0.1", port 5432 +2018-07-23 21:50:52.269 JST [90475] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432" +2018-07-23 21:50:52.400 JST [90476] LOG: database system was shut down at 2018-07-23 16:48:21 JST +2018-07-23 21:50:52.419 JST [90475] LOG: database system is ready to accept connections +2018-07-23 21:51:02.976 JST [90575] ERROR: column upcoming_events.service_name does not exist at character 37 +2018-07-23 21:51:02.976 JST [90575] STATEMENT: DELETE FROM "upcoming_events" WHERE "upcoming_events"."service_name" = $1