├── .env.sample ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── .travis.yml ├── CHECKLIST.md ├── Dockerfile.dev ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── app.json ├── bin ├── console └── setup ├── esa_feeder.gemspec ├── lib ├── esa_feeder.rb ├── esa_feeder │ ├── entities │ │ └── esa_post.rb │ ├── gateways │ │ ├── esa_client.rb │ │ └── slack_client.rb │ ├── use_cases │ │ ├── feed.rb │ │ └── source_tag.rb │ └── version.rb └── tasks │ └── templates.thor ├── logo.png └── spec ├── entities └── esa_post_spec.rb ├── factories └── esa_post.rb ├── gateways ├── esa_client_spec.rb └── slack_client_spec.rb ├── spec_helper.rb ├── support └── factory_bot.rb └── use_cases ├── esa_feeder_spec.rb └── source_tag_spec.rb /.env.sample: -------------------------------------------------------------------------------- 1 | ESA_TEAM=your_team 2 | ESA_OWNER_API_TOKEN=your_token 3 | SLACK_WEBHOOK_URL= 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ((事前に書いてある文書は説明用のものです。 レビュー前には消してください)) 2 | 3 | ## :sparkles: 目的 4 | 5 | * このPRをマージすると以下のことができるようになるよ! 6 | * 仕様書や関連プルリクエストへのリンク 7 | 8 | ## :muscle: 方針 9 | 10 | * 何を考えてこのプルリクを作ったか。 11 | * 考えた結果、除外したものがあれば書いてほしい 12 | 13 | ## :wrench: 実装 14 | 15 | * 方針に沿って実装していく中でレビュアーに伝えたいこと 16 | * 適宜、図や疑似コードを使う 17 | * コードへのコメントでもよい 18 | 19 | ## :white_check_mark: テスト 20 | 21 | * マージボタンを押してもらうための材料 22 | * 確認した項目など 23 | 24 | ## :speech_balloon: 相談 25 | 26 | * 書いてみたけど自信がないところとか 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | ruby_test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Setup Ruby 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | bundler-cache: true 18 | - name: test 19 | run: bundle exec rspec spec/ 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | .env 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - .rubocop_todo.yml 3 | 4 | Style/Documentation: 5 | Enabled: false 6 | 7 | Metrics/LineLength: 8 | Max: 128 9 | 10 | Metrics/AbcSize: 11 | Max: 20 12 | 13 | Metrics/BlockLength: 14 | Exclude: 15 | - 'spec/**/*' 16 | ExcludedMethods: Struct.new 17 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | Metrics/BlockLength: 2 | Enabled: false 3 | 4 | Layout/EmptyLineAfterGuardClause: 5 | Enabled: false 6 | 7 | Naming/MemoizedInstanceVariableName: 8 | Enabled: false 9 | 10 | Style/ExpandPathArguments: 11 | Enabled: false 12 | 13 | Layout/LeadingBlankLines: 14 | Enabled: false 15 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.4.1 5 | before_install: gem install bundler -v 1.15.4 6 | -------------------------------------------------------------------------------- /CHECKLIST.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standfirm/esa_feeder/cd906e4b666d599f713fd4d6f79b35ed67e181f8/CHECKLIST.md -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM ruby:2.7.6-alpine3.16 2 | 3 | RUN apk update && apk add g++ git make 4 | 5 | RUN mkdir /app 6 | COPY Gemfile Gemfile.lock .ruby-version esa_feeder.gemspec /app/ 7 | RUN mkdir -p /app/lib/esa_feeder 8 | COPY lib/esa_feeder/version.rb /app/lib/esa_feeder/ 9 | 10 | WORKDIR /app 11 | RUN bundle install 12 | 13 | VOLUME /app 14 | CMD ["/bin/sh"] 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | ruby File.read('.ruby-version').chomp 6 | 7 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 8 | 9 | # Specify your gem's dependencies in esa_feeder.gemspec 10 | gemspec 11 | 12 | gem 'dotenv' 13 | gem 'esa' 14 | gem 'holiday_japan' 15 | gem 'pry' 16 | gem 'slack-notifier' 17 | gem 'thor' 18 | 19 | group :development, :test do 20 | gem 'rubocop' 21 | end 22 | 23 | group :test do 24 | gem 'factory_bot' 25 | end 26 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | esa_feeder (0.1.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activesupport (7.0.4.2) 10 | concurrent-ruby (~> 1.0, >= 1.0.2) 11 | i18n (>= 1.6, < 2) 12 | minitest (>= 5.1) 13 | tzinfo (~> 2.0) 14 | ast (2.4.0) 15 | coderay (1.1.2) 16 | concurrent-ruby (1.2.0) 17 | diff-lcs (1.3) 18 | dotenv (2.7.2) 19 | esa (1.14.0) 20 | faraday (~> 0.9) 21 | faraday_middleware 22 | mime-types (>= 2.6) 23 | multi_xml (>= 0.5.5) 24 | factory_bot (5.0.2) 25 | activesupport (>= 4.2.0) 26 | faraday (0.15.4) 27 | multipart-post (>= 1.2, < 3) 28 | faraday_middleware (0.13.1) 29 | faraday (>= 0.7.4, < 1.0) 30 | holiday_japan (1.4.3) 31 | i18n (1.12.0) 32 | concurrent-ruby (~> 1.0) 33 | jaro_winkler (1.5.2) 34 | method_source (0.9.2) 35 | mime-types (3.2.2) 36 | mime-types-data (~> 3.2015) 37 | mime-types-data (3.2019.0331) 38 | minitest (5.17.0) 39 | multi_xml (0.6.0) 40 | multipart-post (2.1.1) 41 | parallel (1.17.0) 42 | parser (2.6.3.0) 43 | ast (~> 2.4.0) 44 | pry (0.12.2) 45 | coderay (~> 1.1.0) 46 | method_source (~> 0.9.0) 47 | rainbow (3.0.0) 48 | rake (13.0.1) 49 | rspec (3.8.0) 50 | rspec-core (~> 3.8.0) 51 | rspec-expectations (~> 3.8.0) 52 | rspec-mocks (~> 3.8.0) 53 | rspec-core (3.8.0) 54 | rspec-support (~> 3.8.0) 55 | rspec-expectations (3.8.3) 56 | diff-lcs (>= 1.2.0, < 2.0) 57 | rspec-support (~> 3.8.0) 58 | rspec-mocks (3.8.0) 59 | diff-lcs (>= 1.2.0, < 2.0) 60 | rspec-support (~> 3.8.0) 61 | rspec-support (3.8.0) 62 | rubocop (0.71.0) 63 | jaro_winkler (~> 1.5.1) 64 | parallel (~> 1.10) 65 | parser (>= 2.6) 66 | rainbow (>= 2.2.2, < 4.0) 67 | ruby-progressbar (~> 1.7) 68 | unicode-display_width (>= 1.4.0, < 1.7) 69 | ruby-progressbar (1.10.1) 70 | slack-notifier (2.3.2) 71 | thor (0.20.3) 72 | tzinfo (2.0.6) 73 | concurrent-ruby (~> 1.0) 74 | unicode-display_width (1.6.0) 75 | 76 | PLATFORMS 77 | ruby 78 | 79 | DEPENDENCIES 80 | bundler (~> 1.15) 81 | dotenv 82 | esa 83 | esa_feeder! 84 | factory_bot 85 | holiday_japan 86 | pry 87 | rake (~> 13.0) 88 | rspec (~> 3.0) 89 | rubocop 90 | slack-notifier 91 | thor 92 | 93 | RUBY VERSION 94 | ruby 2.7.6p219 95 | 96 | BUNDLED WITH 97 | 2.3.19 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Yayoi Co., Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > 現在Misoca内でこのツールは使用していないため、アーカイブにします 3 | 4 | # EsaFeeder 5 | 6 | [![CI](https://github.com/standfirm/esa_feeder/actions/workflows/ci.yml/badge.svg)](https://github.com/standfirm/esa_feeder/actions/workflows/ci.yml) 7 | 8 | [esa.io](https://esa.io/) のテンプレートから定期的に記事を自動作成するツールです。 9 | 10 | ## Features 11 | 12 | 繰り返し作成したい記事テンプレートに`#feed_xxx` タグを付けることで、そのテンプレートから記事を自動作成することができます。 13 | `xxx`部分は記事を作成したい曜日の短縮名、もしくは`wday`、'eday'です。以下のタグが使用できます。 14 | 15 | - `#feed_sun` 16 | - `#feed_mon` 17 | - `#feed_tue` 18 | - `#feed_wed` 19 | - `#feed_thu` 20 | - `#feed_fri` 21 | - `#feed_sat` 22 | - `#feed_wday` 23 | - `#feed_eday` 24 | 25 | `#feed_mon`を付けた記事は月曜日のみ自動記事作成の対象になります。 26 | 複数指定も可能で、`#feed_mon #feed_fri`とすれば月曜日と金曜日に自動記事作成の対象になります。 27 | 28 | `#feed_wday`をつけることで、平日(祝日を除く月曜日から金曜日)に自動作成されるようになります。 29 | 日報などはこちらを使うと便利です。 30 | 31 | `#feed_eday`をつけることで、毎日自動作成されるようになります。 32 | 33 | なお、投稿テンプレートは通常のテンプレート(`templates/`以下)の他にユーザごとのテンプレート(`Users/[screen_name]/templates/`以下)にも対応しています。 34 | 35 | ### Notify to Slack 36 | 37 | 自動作成時に、Slackの任意のChannelへ通知することができます。 38 | `#slack_xxx` のようなタグを付けるとSlackの `#xxx` へ通知されます。 39 | 複数指定も可能です。 40 | 41 | 指定が無い場合は通知は飛びません。WebHook側の設定は上書きされます。 42 | 43 | この機能を有効にするにはSLACK_WEBHOOK_URLの設定が必要です。 44 | 45 | ### Specify created by 46 | 47 | 記事の作成ユーザは、通常次の通りになります。 48 | 49 | - 通常のテンプレート(`templates/`以下): `esa_bot` 50 | - ユーザごとのテンプレート(`Users/[screen_name]/templates/`以下): `screen_name`のユーザ 51 | 52 | このうち通常のテンプレートについては `#me_xxx` タグをつけることで作成ユーザを変更することができます。 53 | `xxx`の部分にはユーザのスクリーンネームを指定してください。 54 | 55 | ## How to Run 56 | 57 | ### Run Locally 58 | 59 | ``` 60 | $ git checkout git@github.com:standfirm/esa_feeder.git 61 | $ cd esa_feeder 62 | $ bundle install 63 | $ cp .env.sample .env 64 | $ vi .env 65 | // setup environment variable 66 | ``` 67 | 68 | で環境構築を行ってください。 69 | `bundle exec thor templates:feed`を実行することで、記事の自動作成が行われます。 70 | 毎日実行されるようcronを設定することで、曜日に応じた記事が自動生成されるようになります。 71 | 72 | ### Run on Heroku 73 | 74 | 以下のボタンから[Heroku](https://dashboard.heroku.com/)にデプロイできます。 75 | 76 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 77 | 78 | 79 | 環境変数については、esaのチーム名とアクセストークンが指定必須です。 80 | Slack WebHook URLは入力すると記事が自動作成されたことを通知します。空の場合は通知をスキップします。 81 | 82 | その後、Heroku Scheduler上で`bundle exec thor templates:feed`が毎日実行されるよう設定を行ってください。 83 | この際UTCでの設定になるため時差に注意してください。(0時に設定すると、日本時間9時に実行される) 84 | 85 | ## Test 86 | 87 | ``` 88 | $ bundle exec rspec spec/ 89 | ``` 90 | 91 | でテストを実行できます。 92 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esa feeder", 3 | "description": "periodic post generator for esa.io", 4 | "repository": "https://github.com/standfirm/esa_feeder", 5 | "logo": "https://raw.githubusercontent.com/standfirm/esa_feeder/master/logo.png", 6 | "keywords": ["ruby", "esa.io"], 7 | "env": { 8 | "ESA_TEAM": { 9 | "description": "esa.io team name" 10 | }, 11 | "ESA_OWNER_API_TOKEN": { 12 | "description": "esa.io personal access token. Get from https://your_team.esa.io/user/applications" 13 | }, 14 | "SLACK_WEBHOOK_URL": { 15 | "description": "Slack incoming webhook url. Get from https://your_team.slack.com/apps/manage/custom-integrations. If this variable is empty, then notification is disabled.", 16 | "required": false 17 | }, 18 | "LANG": { 19 | "description": "Language Code", 20 | "value": "en_US.UTF-8" 21 | }, 22 | "TZ": { 23 | "description": "Timezone Code", 24 | "value": "Asia/Tokyo" 25 | } 26 | }, 27 | "addons": ["scheduler"] 28 | } 29 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'esa_feeder' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | require 'dotenv/load' 10 | require 'esa' 11 | 12 | def client 13 | @client ||= Esa::Client.new( 14 | access_token: ENV['ESA_OWNER_API_TOKEN'], 15 | current_team: ENV['ESA_TEAM'] 16 | ) 17 | end 18 | 19 | # (If you use this, don't forget to add pry to your Gemfile!) 20 | require 'pry' 21 | Pry.start 22 | 23 | # require "irb" 24 | # IRB.start(__FILE__) 25 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /esa_feeder.gemspec: -------------------------------------------------------------------------------- 1 | 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path('../lib', __FILE__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | require 'esa_feeder/version' 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = 'esa_feeder' 10 | spec.version = EsaFeeder::VERSION 11 | spec.authors = ['kokuyouwind'] 12 | spec.email = ['kokuyouwind@gmail.com'] 13 | 14 | spec.summary = 'esa.io post generator from templates' 15 | spec.description = 'esa.io post generator from templates' 16 | spec.homepage = 'https://github.com/standfirm/esa-feeder' 17 | 18 | # Prevent pushing this gem to RubyGems.org. 19 | # To allow pushes either set the 'allowed_push_host' 20 | # to allow pushing to a single host or delete this section 21 | # to allow pushing to any host. 22 | if spec.respond_to?(:metadata) 23 | spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" 24 | else 25 | raise 'RubyGems 2.0 or newer is required to protect against ' \ 26 | 'public gem pushes.' 27 | end 28 | 29 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 30 | f.match(%r{^(test|spec|features)/}) 31 | end 32 | spec.bindir = 'exe' 33 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 34 | spec.require_paths = ['lib'] 35 | 36 | spec.add_development_dependency 'bundler', '~> 1.15' 37 | spec.add_development_dependency 'rake', '~> 13.0' 38 | spec.add_development_dependency 'rspec', '~> 3.0' 39 | end 40 | -------------------------------------------------------------------------------- /lib/esa_feeder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dotenv/load' 4 | 5 | require 'esa_feeder/version' 6 | require 'esa_feeder/entities/esa_post' 7 | require 'esa_feeder/use_cases/feed' 8 | require 'esa_feeder/use_cases/source_tag' 9 | require 'esa_feeder/gateways/esa_client' 10 | require 'esa_feeder/gateways/slack_client' 11 | 12 | module EsaFeeder 13 | class << self 14 | def feed 15 | UseCases::Feed.new(esa_client, slack_client).call(UseCases::SourceTag.new.call) 16 | end 17 | 18 | private 19 | 20 | def esa_client 21 | driver = Esa::Client.new( 22 | access_token: ENV['ESA_OWNER_API_TOKEN'], 23 | current_team: ENV['ESA_TEAM'] 24 | ) 25 | @esa_clinet ||= Gateways::EsaClient.new(driver) 26 | end 27 | 28 | def slack_client 29 | return unless ENV['SLACK_WEBHOOK_URL'] 30 | @slack_client ||= Gateways::SlackClient.new( 31 | Slack::Notifier.new(ENV['SLACK_WEBHOOK_URL']) 32 | ) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/esa_feeder/entities/esa_post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module EsaFeeder 4 | module Entities 5 | EsaPost = Struct.new(:number, :category, :name, :url, :tags) do 6 | def slack_channels 7 | slack_tags.map { |tag| tag.gsub(/^slack_/, '') } 8 | end 9 | 10 | def full_name 11 | category ? "#{category}/#{name}" : name 12 | end 13 | 14 | def user_tags 15 | tags - system_tags 16 | end 17 | 18 | def feed_users 19 | if private_template? 20 | [category_user] 21 | elsif me_tags_present? 22 | me_tags.map { |tag| tag.gsub(/^me_/, '') } 23 | else 24 | ['esa_bot'] 25 | end 26 | end 27 | 28 | private 29 | 30 | def system_tags 31 | feed_tags + slack_tags + me_tags 32 | end 33 | 34 | def feed_tags 35 | tags.select { |tag| tag =~ /^feed_/ } 36 | end 37 | 38 | def slack_tags 39 | tags.select { |tag| tag =~ /^slack_/ } 40 | end 41 | 42 | def me_tags_present? 43 | !me_tags.empty? 44 | end 45 | 46 | def me_tags 47 | tags.select { |tag| tag =~ /^me_/ } 48 | end 49 | 50 | def private_template? 51 | !category_user.nil? 52 | end 53 | 54 | def category_user 55 | category[%r{Users/(\w+)/(t|T)emplates}, 1] 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/esa_feeder/gateways/esa_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'esa' 4 | 5 | module EsaFeeder 6 | module Gateways 7 | class EsaClient 8 | class PostCreateError < StandardError; end 9 | class PostUpdateError < StandardError; end 10 | 11 | def initialize(driver) 12 | @driver = driver 13 | end 14 | 15 | def find_templates(tag) 16 | response = driver.posts(q: "category:templates -in:Archived tag:#{tag} OR category:Templates -in:Archived tag:#{tag}") 17 | to_posts(response.body) 18 | end 19 | 20 | def create_from_template(post, user) 21 | response = driver.create_post(template_post_id: post.number, user: user) 22 | # error happen when user does not exists 23 | raise PostCreateError, response.body['message'] if response.body['error'] 24 | to_post(response.body) 25 | end 26 | 27 | def update_post(post, user) 28 | response = driver.update_post( 29 | post.number, tags: post.tags, updated_by: user 30 | ) 31 | # error happen when user does not exists 32 | raise PostUpdateError, response.body['message'] if response.body['error'] 33 | to_post(response.body) 34 | end 35 | 36 | private 37 | 38 | attr_reader :driver 39 | 40 | def to_posts(body) 41 | body['posts'].map { |raw| to_post(raw) } 42 | end 43 | 44 | def to_post(raw) 45 | Entities::EsaPost.new( 46 | raw['number'], 47 | raw['category'], 48 | raw['name'], 49 | raw['url'], 50 | raw['tags'] 51 | ) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/esa_feeder/gateways/slack_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'slack-notifier' 4 | 5 | module EsaFeeder 6 | module Gateways 7 | class SlackClient 8 | def initialize(driver) 9 | @driver = driver 10 | end 11 | 12 | def notify_creation(message, post) 13 | driver.post attachments: [{ 14 | pretext: message, 15 | title: post.full_name, 16 | title_link: post.url, 17 | color: 'good' 18 | }], 19 | channel: post.slack_channels 20 | rescue Slack::Notifier::APIError => e 21 | puts e.inspect 22 | nil 23 | end 24 | 25 | private 26 | 27 | attr_reader :driver 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/esa_feeder/use_cases/feed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module EsaFeeder 4 | module UseCases 5 | class Feed 6 | def initialize(esa_port, notifier_port) 7 | @esa_port = esa_port 8 | @notifier_port = notifier_port 9 | end 10 | 11 | def call(tags) 12 | find_templates(tags).map do |template| 13 | template.feed_users.map do |feed_user| 14 | feed_post(template, feed_user) 15 | end 16 | end.flatten 17 | end 18 | 19 | private 20 | 21 | attr_reader :esa_port, :notifier_port 22 | 23 | def find_templates(tags) 24 | tags.map do |tag| 25 | esa_port.find_templates(tag) 26 | end.flatten 27 | end 28 | 29 | def feed_post(template, feed_user) 30 | post = esa_port.create_from_template(template, feed_user) 31 | notifier_port&.notify_creation('新しい記事を作成しました', post) 32 | # remove system tags from generated post 33 | post.tags = post.user_tags 34 | esa_port.update_post(post, feed_user) 35 | { template.number => post.number } 36 | rescue StandardError => e 37 | # continue other processing 38 | puts e.inspect 39 | {} 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/esa_feeder/use_cases/source_tag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'holiday_japan' 4 | 5 | module EsaFeeder 6 | module UseCases 7 | class SourceTag 8 | def initialize(time = nil) 9 | @time = time || Time.now 10 | end 11 | 12 | def call 13 | [day_of_the_week_feed, weekday_feed, everyday_feed].reject(&:nil?) 14 | end 15 | 16 | private 17 | 18 | attr_reader :time 19 | 20 | def day_of_the_week_feed 21 | "feed_#{time.strftime('%a').downcase}" 22 | end 23 | 24 | def weekday_feed 25 | 'feed_wday' if (1..5).cover?(time.wday) && !holiday? 26 | end 27 | 28 | def everyday_feed 29 | 'feed_eday' 30 | end 31 | 32 | def holiday? 33 | HolidayJapan.check(time.to_date) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/esa_feeder/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module EsaFeeder 4 | VERSION = '0.1.0' 5 | end 6 | -------------------------------------------------------------------------------- /lib/tasks/templates.thor: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'esa_feeder' 4 | require 'logger' 5 | 6 | class Templates < Thor 7 | desc 'feed', 'create posts from templates with wday tag' 8 | def feed 9 | logger = Logger.new(STDOUT) 10 | logger.info('feed task start') 11 | begin 12 | logger.info(EsaFeeder.feed) 13 | rescue StandardError => e 14 | logger.error(e) 15 | end 16 | logger.info('feed task finished') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standfirm/esa_feeder/cd906e4b666d599f713fd4d6f79b35ed67e181f8/logo.png -------------------------------------------------------------------------------- /spec/entities/esa_post_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe EsaFeeder::Entities::EsaPost do 6 | describe '#initialize' do 7 | let(:post) do 8 | described_class.new( 9 | 99_999, 10 | 'path/to/post', 11 | 'post_title', 12 | 'https://example.com/posts/99999', 13 | %w[feed_mon feed_tue] 14 | ) 15 | end 16 | 17 | it 'can assign values' do 18 | expect(post.number).to eq(99_999) 19 | expect(post.category).to eq('path/to/post') 20 | expect(post.name).to eq('post_title') 21 | expect(post.url).to eq('https://example.com/posts/99999') 22 | expect(post.tags).to eq(%w[feed_mon feed_tue]) 23 | end 24 | end 25 | 26 | describe '#slack_channels' do 27 | subject { post.slack_channels } 28 | 29 | context 'with slack tags' do 30 | let(:post) { build(:esa_template, :with_slack_tag) } 31 | it 'return only slack tag' do 32 | expect(subject).to eq(['hoge']) 33 | end 34 | end 35 | 36 | context 'with no slack tags' do 37 | let(:post) { build(:esa_template) } 38 | it 'return empty array' do 39 | expect(subject).to eq([]) 40 | end 41 | end 42 | end 43 | 44 | describe '#full_name' do 45 | subject { post.full_name } 46 | 47 | context 'in root' do 48 | let(:post) { build(:esa_post, category: nil, name: 'test_post') } 49 | it { expect(subject).to eq('test_post') } 50 | end 51 | 52 | context 'in category' do 53 | let(:post) do 54 | build(:esa_post, category: 'path/to/post', name: 'test_post') 55 | end 56 | it { expect(subject).to eq('path/to/post/test_post') } 57 | end 58 | end 59 | 60 | describe '#user_tags' do 61 | let(:post) { build(:esa_post, tags: tags) } 62 | subject { post.user_tags } 63 | 64 | context 'not contain system_tags' do 65 | let(:tags) { %w[hoge fuga] } 66 | it { expect(subject).to eq(%w[hoge fuga]) } 67 | end 68 | 69 | context 'contain system_tags' do 70 | let(:tags) { %w[hoge feed_mon feed_fri slack_test fuga] } 71 | it { expect(subject).to eq(%w[hoge fuga]) } 72 | end 73 | end 74 | 75 | describe '#feed_users' do 76 | let(:category) { 'templates/path/to/post' } 77 | let(:post) { build(:esa_template, category: category, tags: tags) } 78 | subject { post.feed_users } 79 | 80 | context 'not contain me_tags' do 81 | let(:tags) { %w[hoge fuga] } 82 | it { expect(subject).to eq(%w[esa_bot]) } 83 | end 84 | 85 | context 'contain me_tags' do 86 | let(:tags) { %w[hoge fuga me_test] } 87 | it { expect(subject).to eq(%w[test]) } 88 | end 89 | 90 | context 'user private templates' do 91 | let(:tags) { %w[hoge fuga me_test] } 92 | let(:category) { 'Users/piyo/templates/path/to/post' } 93 | it { expect(subject).to eq(%w[piyo]) } 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/factories/esa_post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :esa_post, class: EsaFeeder::Entities::EsaPost do 5 | sequence :number 6 | category { 'path/to/post' } 7 | name { "post_title_#{number}" } 8 | url { "https://example.com/posts/#{number}" } 9 | tags { %w[tag] } 10 | 11 | factory :esa_template do 12 | tags { %w[tag feed_mon] } 13 | category { 'templates/path/to/post' } 14 | 15 | trait :with_slack_tag do 16 | tags { %w[tag feed_mon slack_hoge hoge_slack_fuga] } 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/gateways/esa_client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe EsaFeeder::Gateways::EsaClient do 6 | let(:driver) { double('driver') } 7 | let(:target) { described_class.new(driver) } 8 | let(:template) { build(:esa_template) } 9 | 10 | describe '#find_templates' do 11 | let(:body) do 12 | { 'posts' => [ 13 | 'number' => template.number, 14 | 'category' => template.category, 15 | 'name' => template.name, 16 | 'url' => template.url, 17 | 'tags' => template.tags 18 | ] } 19 | end 20 | let(:response) { double('response', body: body) } 21 | 22 | subject { target.find_templates('test_tag') } 23 | 24 | it 'return templates' do 25 | query = 'category:templates -in:Archived tag:test_tag OR category:Templates -in:Archived tag:test_tag' 26 | 27 | allow(driver).to receive(:posts).with(q: query).and_return(response) 28 | expect(subject).to eq([template]) 29 | end 30 | end 31 | 32 | describe '#create_from_template' do 33 | let(:post) { build(:esa_post) } 34 | let(:body) do 35 | { 'number' => post.number, 36 | 'category' => post.category, 37 | 'name' => post.name, 38 | 'url' => post.url, 39 | 'tags' => post.tags } 40 | end 41 | let(:response) { double('response', body: body) } 42 | 43 | before do 44 | allow(driver).to receive(:create_post) 45 | .with(template_post_id: template.number, user: 'bot_user') 46 | .and_return(response) 47 | end 48 | subject { target.create_from_template(template, 'bot_user') } 49 | 50 | it 'return created post' do 51 | expect(subject).to eq(post) 52 | end 53 | 54 | context 'api returns error' do 55 | let(:body) { { 'error' => 'some errors' } } 56 | 57 | it 'raise exeption' do 58 | expect { subject }.to raise_exception(EsaFeeder::Gateways::EsaClient::PostCreateError) 59 | end 60 | end 61 | end 62 | 63 | describe '#update_post' do 64 | let(:post) { build(:esa_post) } 65 | let(:body) do 66 | { 'number' => post.number, 67 | 'category' => post.category, 68 | 'name' => post.name, 69 | 'url' => post.url, 70 | 'tags' => post.tags } 71 | end 72 | let(:response) { double('response', body: body) } 73 | 74 | before do 75 | allow(driver).to receive(:update_post) 76 | .with(post.number, tags: post.tags, updated_by: 'bot_user') 77 | .and_return(response) 78 | end 79 | 80 | subject { target.update_post(post, 'bot_user') } 81 | 82 | it 'return updated post' do 83 | expect(subject).to eq(post) 84 | end 85 | 86 | context 'api returns error' do 87 | let(:body) { { 'error' => 'some errors' } } 88 | 89 | it 'raise exeption' do 90 | expect { subject }.to raise_exception(EsaFeeder::Gateways::EsaClient::PostUpdateError) 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/gateways/slack_client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe EsaFeeder::Gateways::SlackClient do 6 | let(:post) { build(:esa_template, :with_slack_tag) } 7 | let(:driver) { double('slack notifier') } 8 | let(:target) { described_class.new(driver) } 9 | 10 | before do 11 | allow(driver).to receive(:post) 12 | end 13 | 14 | it '#notify_creation' do 15 | expect(driver) 16 | .to receive(:post) 17 | .once 18 | .with(attachments: [{ 19 | pretext: 'message', 20 | title: post.full_name, 21 | title_link: post.url, 22 | color: 'good' 23 | }], 24 | channel: ['hoge']) 25 | 26 | target.notify_creation('message', post) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'esa_feeder' 5 | require 'support/factory_bot' 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = '.rspec_status' 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'factory_bot' 4 | 5 | RSpec.configure do |config| 6 | config.include FactoryBot::Syntax::Methods 7 | 8 | config.before(:suite) do 9 | FactoryBot.find_definitions 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/use_cases/esa_feeder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe EsaFeeder::UseCases::Feed do 6 | let(:esa_client) { double('esa client') } 7 | let(:feed_users) { %w[esa_bot] } 8 | 9 | subject do 10 | described_class.new(esa_client, slack_client).call([tag]) 11 | end 12 | 13 | let(:expected) do 14 | [ 15 | { templates[0].number => posts[0].number }, 16 | { templates[1].number => posts[1].number } 17 | ] 18 | end 19 | 20 | shared_context 'mock esa api' do |template_no, post_no, feed_user| 21 | before do 22 | expect(esa_client).to receive(:create_from_template) 23 | .with(templates[template_no], feed_user).once 24 | .and_return(posts[post_no]) 25 | expect(esa_client).to receive(:update_post) 26 | .with( 27 | have_attributes(number: posts[post_no].number, 28 | tags: %w[hoge fuga]), feed_user 29 | ).once 30 | end 31 | end 32 | 33 | shared_examples 'create post from templates' do 34 | before do 35 | expect(esa_client).to receive(:find_templates) 36 | .with(tag).once 37 | .and_return(templates) 38 | end 39 | 40 | context 'notifier provided' do 41 | let(:slack_client) { double('slack client') } 42 | it 'create posts and notify' do 43 | expect(slack_client).to receive(:notify_creation).exactly(expected.length) 44 | expect(subject).to eq(expected) 45 | end 46 | end 47 | 48 | context 'notifier not provided' do 49 | let(:slack_client) { nil } 50 | it 'create posts' do 51 | expect(subject).to eq(expected) 52 | end 53 | end 54 | end 55 | 56 | context 'mon_templates' do 57 | let(:templates) { build_list(:esa_template, 2) } 58 | let(:tag) { 'feed_mon' } 59 | let(:posts) do 60 | build_list(:esa_post, 2, tags: %w[hoge feed_mon fuga slack_test]) 61 | end 62 | 63 | include_context 'mock esa api', 0, 0, 'esa_bot' 64 | include_context 'mock esa api', 1, 1, 'esa_bot' 65 | it_behaves_like 'create post from templates' 66 | end 67 | 68 | context 'wday_templates' do 69 | let(:templates) { build_list(:esa_template, 2, tags: %w[tag feed_wday]) } 70 | let(:tag) { 'feed_wday' } 71 | let(:posts) do 72 | build_list(:esa_post, 2, tags: %w[hoge feed_wday fuga slack_test]) 73 | end 74 | 75 | include_context 'mock esa api', 0, 0, 'esa_bot' 76 | include_context 'mock esa api', 1, 1, 'esa_bot' 77 | it_behaves_like 'create post from templates' 78 | end 79 | 80 | context 'everyday_templates' do 81 | let(:templates) { build_list(:esa_template, 2, tags: %w[tag feed_eday]) } 82 | let(:tag) { 'feed_eday' } 83 | let(:posts) do 84 | build_list(:esa_post, 2, tags: %w[hoge feed_eday fuga slack_test]) 85 | end 86 | 87 | include_context 'mock esa api', 0, 0, 'esa_bot' 88 | include_context 'mock esa api', 1, 1, 'esa_bot' 89 | it_behaves_like 'create post from templates' 90 | end 91 | 92 | context 'me_templates' do 93 | let(:templates) { build_list(:esa_template, 1, tags: %w[tag feed_wday me_hoge me_fuga]) } 94 | let(:tag) { 'feed_wday' } 95 | let(:feed_users) { %w[hoge fuga] } 96 | let(:posts) do 97 | build_list(:esa_post, 2, tags: %w[hoge feed_wday fuga me_hoge me_fuga]) 98 | end 99 | 100 | let(:expected) do 101 | [ 102 | { templates[0].number => posts[0].number }, 103 | { templates[0].number => posts[1].number } 104 | ] 105 | end 106 | 107 | include_context 'mock esa api', 0, 0, 'hoge' 108 | include_context 'mock esa api', 0, 1, 'fuga' 109 | it_behaves_like 'create post from templates' 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/use_cases/source_tag_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe EsaFeeder::UseCases::SourceTag do 6 | let(:tags) do 7 | (0..6).map { |n| described_class.new(Time.local(2017, 4, 2 + n)).call } 8 | end 9 | let(:holiday_tags) { described_class.new(Time.local(2017, 11, 23)).call } 10 | 11 | let(:ref) { %w[sun mon tue wed thu fri sat] } 12 | 13 | it 'include tag each day of the week' do 14 | (0..6).map { |n| expect(tags[n]).to include("feed_#{ref[n]}") } 15 | end 16 | 17 | it 'include tag #feed_wday on weekdays' do 18 | (1..5).map { |n| expect(tags[n]).to include('feed_wday') } 19 | end 20 | 21 | it 'include tag #feed_eday on everyday' do 22 | (0..6).map { |n| expect(tags[n]).to include('feed_eday') } 23 | end 24 | 25 | it 'do not include tag #feed_wday on weekend' do 26 | expect(tags[0]).not_to include('feed_wday') 27 | expect(tags[6]).not_to include('feed_wday') 28 | expect(tags[0]).to include('feed_eday') 29 | expect(tags[6]).to include('feed_eday') 30 | end 31 | 32 | it 'do not include tag #feed_wday on public holiday' do 33 | expect(holiday_tags).not_to include('feed_wday') 34 | expect(holiday_tags).to include('feed_thu') 35 | expect(holiday_tags).to include('feed_eday') 36 | end 37 | end 38 | --------------------------------------------------------------------------------