From 6deb9f966eb9a280cc16428ba9324ffc15ea60a8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 18 Aug 2016 15:49:51 +0200 Subject: [PATCH] Live timelines using ActionCable --- Gemfile | 3 +- Gemfile.lock | 4 --- app/assets/javascripts/api/accounts.coffee | 3 -- .../javascripts/api/accounts/lookup.coffee | 3 -- app/assets/javascripts/api/follows.coffee | 3 -- app/assets/javascripts/api/statuses.coffee | 3 -- app/assets/javascripts/application.js | 1 - app/assets/javascripts/cable.js | 13 +++++++ app/assets/javascripts/channels/timeline.js | 13 +++++++ .../javascripts/oauth/applications.coffee | 3 -- app/assets/javascripts/profiler.coffee | 5 --- app/assets/javascripts/settings.coffee | 3 -- app/assets/javascripts/statuses.coffee | 3 -- app/channels/application_cable/channel.rb | 5 +++ app/channels/application_cable/connection.rb | 20 +++++++++++ app/channels/timeline_channel.rb | 10 ++++++ app/controllers/application_controller.rb | 2 +- app/services/fan_out_on_write_service.rb | 35 +++++++++++++------ app/services/precompute_feed_service.rb | 2 +- config/cable.yml | 3 +- config/environments/development.rb | 3 ++ config/initializers/assets.rb | 2 +- config/initializers/rack-mini-profiler.rb | 8 ++--- config/routes.rb | 2 ++ 24 files changed, 99 insertions(+), 53 deletions(-) delete mode 100644 app/assets/javascripts/api/accounts.coffee delete mode 100644 app/assets/javascripts/api/accounts/lookup.coffee delete mode 100644 app/assets/javascripts/api/follows.coffee delete mode 100644 app/assets/javascripts/api/statuses.coffee create mode 100644 app/assets/javascripts/cable.js create mode 100644 app/assets/javascripts/channels/timeline.js delete mode 100644 app/assets/javascripts/oauth/applications.coffee delete mode 100644 app/assets/javascripts/profiler.coffee delete mode 100644 app/assets/javascripts/settings.coffee delete mode 100644 app/assets/javascripts/statuses.coffee create mode 100644 app/channels/application_cable/channel.rb create mode 100644 app/channels/application_cable/connection.rb create mode 100644 app/channels/timeline_channel.rb diff --git a/Gemfile b/Gemfile index a0c8de16..6b6ded4d 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,6 @@ gem 'onebox' gem 'simple_form' gem 'will_paginate' gem 'rack-attack' -gem 'turbolinks' gem 'sidekiq' gem 'sinatra', require: nil, github: 'sinatra' @@ -66,5 +65,5 @@ group :production do end group :development, :production do - gem 'rack-mini-profiler', require: false + gem 'rack-mini-profiler' end diff --git a/Gemfile.lock b/Gemfile.lock index 93ff50b6..19b9b7fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -321,9 +321,6 @@ GEM thread_safe (0.3.5) tilt (2.0.5) tool (0.2.3) - turbolinks (5.0.1) - turbolinks-source (~> 5) - turbolinks-source (5.0.0) tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (3.0.1) @@ -394,7 +391,6 @@ DEPENDENCIES simplecov sinatra! therubyracer - turbolinks uglifier (>= 1.3.0) webmock will_paginate diff --git a/app/assets/javascripts/api/accounts.coffee b/app/assets/javascripts/api/accounts.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/api/accounts.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/accounts/lookup.coffee b/app/assets/javascripts/api/accounts/lookup.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/api/accounts/lookup.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/follows.coffee b/app/assets/javascripts/api/follows.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/api/follows.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/statuses.coffee b/app/assets/javascripts/api/statuses.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/api/statuses.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e07c5a83..646c5aba 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,5 +12,4 @@ // //= require jquery //= require jquery_ujs -//= require turbolinks //= require_tree . diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js new file mode 100644 index 00000000..71ee1e66 --- /dev/null +++ b/app/assets/javascripts/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the rails generate channel command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/app/assets/javascripts/channels/timeline.js b/app/assets/javascripts/channels/timeline.js new file mode 100644 index 00000000..ca7c50d1 --- /dev/null +++ b/app/assets/javascripts/channels/timeline.js @@ -0,0 +1,13 @@ +App.timeline = App.cable.subscriptions.create("TimelineChannel", { + connected: function() { + console.log('Connected'); + }, + + disconnected: function() { + console.log('Disconnected'); + }, + + received: function(data) { + console.log(JSON.parse(data.message)); + } +}); diff --git a/app/assets/javascripts/oauth/applications.coffee b/app/assets/javascripts/oauth/applications.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/oauth/applications.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/profiler.coffee b/app/assets/javascripts/profiler.coffee deleted file mode 100644 index 40dfd0af..00000000 --- a/app/assets/javascripts/profiler.coffee +++ /dev/null @@ -1,5 +0,0 @@ -$ -> - $(document).on 'turbolinks:load', -> - unless typeof window.MiniProfiler == 'undefined' - window.MiniProfiler.init() - window.MiniProfiler.pageTransition() diff --git a/app/assets/javascripts/settings.coffee b/app/assets/javascripts/settings.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/settings.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/statuses.coffee b/app/assets/javascripts/statuses.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/statuses.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 00000000..d56fa30f --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 00000000..f7cbe548 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,20 @@ +# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + protected + + def find_verified_user + if verified_user = env['warden'].user + verified_user + else + reject_unauthorized_connection + end + end + end +end diff --git a/app/channels/timeline_channel.rb b/app/channels/timeline_channel.rb new file mode 100644 index 00000000..c128fae5 --- /dev/null +++ b/app/channels/timeline_channel.rb @@ -0,0 +1,10 @@ +# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. +class TimelineChannel < ApplicationCable::Channel + def subscribed + stream_from "timeline:#{current_user.id}" + end + + def unsubscribed + # Any cleanup needed when channel is unsubscribed + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d5eaecdb..f90628b0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base # Profiling before_action do - if current_user && current_user.admin? + if (current_user && current_user.admin?) || Rails.env == 'development' Rack::MiniProfiler.authorize_request end end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index c8c775b9..34684a06 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -10,13 +10,13 @@ class FanOutOnWriteService < BaseService private def deliver_to_self(status) - push(:home, status.account.id, status) + push(:home, status.account, status) end def deliver_to_followers(status) status.account.followers.each do |follower| next if !follower.local? || FeedManager.filter_status?(status, follower) - push(:home, follower.id, status) + push(:home, follower, status) end end @@ -24,23 +24,38 @@ class FanOutOnWriteService < BaseService status.mentions.each do |mention| mentioned_account = mention.account next unless mentioned_account.local? - push(:mentions, mentioned_account.id, status) + push(:mentions, mentioned_account, status) end end - def push(type, receiver_id, status) - redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id) - trim(type, receiver_id) + def push(type, receiver, status) + redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id) + trim(type, receiver) + ActionCable.server.broadcast("timeline:#{receiver.id}", message: inline_render(receiver, status)) end - def trim(type, receiver_id) - return unless redis.zcard(FeedManager.key(type, receiver_id)) > FeedManager::MAX_ITEMS + def trim(type, receiver) + return unless redis.zcard(FeedManager.key(type, receiver.id)) > FeedManager::MAX_ITEMS - last = redis.zrevrange(FeedManager.key(type, receiver_id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1) - redis.zremrangebyscore(FeedManager.key(type, receiver_id), '-inf', "(#{last.last}") + last = redis.zrevrange(FeedManager.key(type, receiver.id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1) + redis.zremrangebyscore(FeedManager.key(type, receiver.id), '-inf', "(#{last.last}") end def redis $redis end + + def inline_render(receiver, status) + rabl_scope = Class.new(BaseService) do + def initialize(account) + @account = account + end + + def current_user + @account.user + end + end + + Rabl::Renderer.new('api/statuses/show', status, view_path: 'app/views', format: :json, scope: rabl_scope.new(receiver)).render + end end diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index de4201a8..c8050bbd 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -8,7 +8,7 @@ class PrecomputeFeedService < BaseService Status.send("as_#{type}_timeline", account).order('created_at desc').limit(FeedManager::MAX_ITEMS).each do |status| next if type == :home && FeedManager.filter_status?(status, account) - redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id) + redis.zadd(FeedManager.key(type, account.id), status.id, status.id) instant_return << status unless instant_return.size > limit end diff --git a/config/cable.yml b/config/cable.yml index b544be4b..978f721a 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -1,5 +1,6 @@ development: - adapter: async + adapter: redis + url: redis://localhost:6379/1 test: adapter: async diff --git a/config/environments/development.rb b/config/environments/development.rb index 1affeca1..ba0af1f5 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -64,3 +64,6 @@ Rails.application.configure do Bullet.rails_logger = true end end + +require 'sidekiq/testing' +Sidekiq::Testing.inline! diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 01ef3e66..23c5b0b6 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -8,4 +8,4 @@ Rails.application.config.assets.version = '1.0' # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w( search.js ) +Rails.application.config.assets.precompile += %w( cable.js ) diff --git a/config/initializers/rack-mini-profiler.rb b/config/initializers/rack-mini-profiler.rb index b1d45e25..7fd50a9a 100644 --- a/config/initializers/rack-mini-profiler.rb +++ b/config/initializers/rack-mini-profiler.rb @@ -1,6 +1,2 @@ -require 'rack-mini-profiler' - -Rack::MiniProfilerRails.initialize!(Rails.application) - -Rails.application.middleware.delete(Rack::MiniProfiler) -Rails.application.middleware.insert_after(Rack::Deflater, Rack::MiniProfiler) +Rails.application.middleware.swap(Rack::Deflater, Rack::MiniProfiler) +Rails.application.middleware.swap(Rack::MiniProfiler, Rack::Deflater) diff --git a/config/routes.rb b/config/routes.rb index e9e662ed..7b6b1ab3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ require 'sidekiq/web' Rails.application.routes.draw do + mount ActionCable.server => '/cable' + authenticate :user, lambda { |u| u.admin? } do mount Sidekiq::Web => '/sidekiq' end