├── log └── .keep ├── tmp └── .keep ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── user_test.rb │ ├── authorization_test.rb │ └── mastodon_client_test.rb ├── controllers │ ├── .keep │ ├── accounts_controller_test.rb │ ├── authorizations_controller_test.rb │ ├── users │ │ └── omniauth_callbacks_controller_test.rb │ ├── home_controller_test.rb │ └── friends_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── mastodon_clients.yml │ ├── authorizations.yml │ └── users.yml ├── integration │ └── .keep └── test_helper.rb ├── .ruby-version ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── preview.jpg │ │ ├── toot-friend.png │ │ ├── twitter-square.png │ │ ├── logo.svg │ │ └── logo_full.svg │ ├── javascripts │ │ ├── application.js │ │ ├── friends.js │ │ ├── home.js │ │ ├── accounts.js │ │ ├── authorizations.js │ │ └── users │ │ │ └── omniauth_callbacks.js │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ ├── accounts.scss │ │ ├── authorizations.scss │ │ ├── application.css │ │ └── home.scss ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── authorization.rb │ ├── mastodon_client.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── accounts_controller.rb │ ├── application_controller.rb │ ├── home_controller.rb │ ├── authorizations_controller.rb │ ├── users │ │ └── omniauth_callbacks_controller.rb │ └── friends_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.haml │ │ ├── mailer.html.haml │ │ └── application.html.haml │ ├── friends │ │ ├── _instances_grid.html.haml │ │ ├── _friends_grid.html.haml │ │ └── index.html.haml │ ├── accounts │ │ └── show.html.haml │ └── home │ │ └── index.html.haml ├── helpers │ ├── home_helper.rb │ ├── accounts_helper.rb │ ├── authorizations_helper.rb │ ├── users │ │ └── omniauth_callbacks_helper.rb │ ├── friends_helper.rb │ └── application_helper.rb └── mailers │ └── application_mailer.rb ├── Procfile ├── vendor └── assets │ └── stylesheets │ └── .keep ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── bin ├── rake ├── bundle ├── rails ├── update └── setup ├── config ├── spring.rb ├── boot.rb ├── environment.rb ├── initializers │ ├── httplog.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ ├── new_framework_defaults.rb │ └── devise.rb ├── routes.rb ├── locales │ ├── en.yml │ └── devise.en.yml ├── application.rb ├── secrets.yml ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── puma.rb └── database.yml ├── config.ru ├── db ├── migrate │ ├── 20161023195605_devise_create_users.rb │ ├── 20180110004149_add_client_token_to_mastodon_clients.rb │ ├── 20170404222753_add_more_info_to_authorizations.rb │ ├── 20161023201417_create_mastodon_clients.rb │ └── 20161023200113_create_authorizations.rb ├── seeds.rb └── schema.rb ├── Rakefile ├── README.md ├── .gitignore ├── Gemfile ├── .rubocop.yml └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.1 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: rails s 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.haml: -------------------------------------------------------------------------------- 1 | = yield 2 | -------------------------------------------------------------------------------- /app/helpers/home_helper.rb: -------------------------------------------------------------------------------- 1 | module HomeHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/accounts_helper.rb: -------------------------------------------------------------------------------- 1 | module AccountsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/authorizations_helper.rb: -------------------------------------------------------------------------------- 1 | module AuthorizationsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | -------------------------------------------------------------------------------- /app/helpers/users/omniauth_callbacks_helper.rb: -------------------------------------------------------------------------------- 1 | module Users::OmniauthCallbacksHelper 2 | end 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mastodon/mastodon-bridge/master/public/favicon.ico -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /app/assets/images/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mastodon/mastodon-bridge/master/app/assets/images/preview.jpg -------------------------------------------------------------------------------- /app/assets/images/toot-friend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mastodon/mastodon-bridge/master/app/assets/images/toot-friend.png -------------------------------------------------------------------------------- /app/assets/images/twitter-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mastodon/mastodon-bridge/master/app/assets/images/twitter-square.png -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/httplog.rb: -------------------------------------------------------------------------------- 1 | HttpLog.configure do |config| 2 | config.logger = Rails.logger 3 | config.color = { color: :yellow } 4 | config.compact_log = true 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/friends.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /app/assets/javascripts/home.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /test/models/authorization_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AuthorizationTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/accounts.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /test/models/mastodon_client_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MastodonClientTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/authorizations.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /app/helpers/friends_helper.rb: -------------------------------------------------------------------------------- 1 | module FriendsHelper 2 | def mastodon_profile_url(uid) 3 | username, domain = uid.split('@') 4 | "https://#{domain}/users/#{username}" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_mastodon-bridge_session' 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/users/omniauth_callbacks.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/controllers/accounts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AccountsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/accounts.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the accounts controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /db/migrate/20161023195605_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | t.timestamps null: false 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/authorizations_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AuthorizationsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180110004149_add_client_token_to_mastodon_clients.rb: -------------------------------------------------------------------------------- 1 | class AddClientTokenToMastodonClients < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :mastodon_clients, :client_token, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/authorizations.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the authorizations controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"}/ 5 | :css 6 | /* Email styles need to be inline */ 7 | %body 8 | = yield 9 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test/controllers/users/omniauth_callbacks_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Users::OmniauthCallbacksControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/controllers/home_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HomeControllerTest < ActionDispatch::IntegrationTest 4 | test "should get index" do 5 | get home_index_url 6 | assert_response :success 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/accounts_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AccountsController < ApplicationController 4 | before_action :authenticate_user! 5 | 6 | def show 7 | @authorizations = current_user.authorizations 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/friends_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class FriendsControllerTest < ActionDispatch::IntegrationTest 4 | test "should get index" do 5 | get friends_index_url 6 | assert_response :success 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | def twitter? 5 | user_signed_in? && !current_user.twitter.nil? 6 | end 7 | 8 | def mastodon? 9 | user_signed_in? && !current_user.mastodon.nil? 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /test/fixtures/mastodon_clients.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | domain: MyString 5 | client_id: MyString 6 | client_secret: MyString 7 | 8 | two: 9 | domain: MyString 10 | client_id: MyString 11 | client_secret: MyString 12 | -------------------------------------------------------------------------------- /db/migrate/20170404222753_add_more_info_to_authorizations.rb: -------------------------------------------------------------------------------- 1 | class AddMoreInfoToAuthorizations < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :authorizations, :profile_url, :string, null: false, default: '' 4 | add_column :authorizations, :display_name, :string, null: false, default: '' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/fixtures/authorizations.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | provider: MyString 5 | uid: MyString 6 | user_id: 1 7 | token: MyString 8 | 9 | two: 10 | provider: MyString 11 | uid: MyString 12 | user_id: 1 13 | token: MyString 14 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | protect_from_forgery with: :exception 5 | 6 | def after_sign_in_path_for(user) 7 | if user.twitter.nil? 8 | root_path 9 | else 10 | friends_path 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20161023201417_create_mastodon_clients.rb: -------------------------------------------------------------------------------- 1 | class CreateMastodonClients < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :mastodon_clients do |t| 4 | t.string :domain 5 | t.string :client_id 6 | t.string :client_secret 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :mastodon_clients, :domain, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mastodon Bridge 2 | 3 | This is a simple web app that helps you find your Twitter friends on the federated Mastodon network. It is also an example of how the Mastodon API can be used and the federated OAuth authorization flow. 4 | 5 | ## Links 6 | 7 | - [Ruby gem for Mastodon API](https://github.com/Gargron/mastodon-api) 8 | - [OmniAuth strategy for Mastodon](https://github.com/Gargron/omniauth-mastodon) 9 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HomeController < ApplicationController 4 | def index 5 | @twitter_count = Rails.cache.fetch('total_twitter_count', expires_in: 15.minutes) { Authorization.where(provider: :twitter).count } 6 | @mastodon_count = Rails.cache.fetch('total_mastodon_count', expires_in: 15.minutes) { Authorization.where(provider: :mastodon).count } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20161023200113_create_authorizations.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthorizations < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :authorizations do |t| 4 | t.string :provider 5 | t.string :uid 6 | t.integer :user_id 7 | t.string :token 8 | t.string :secret 9 | 10 | t.timestamps 11 | end 12 | 13 | add_index :authorizations, [:provider, :uid], unique: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/controllers/authorizations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AuthorizationsController < ApplicationController 4 | before_action :authenticate_user! 5 | before_action :set_authorization 6 | 7 | def destroy 8 | @authorization.destroy 9 | redirect_to account_path, notice: 'Disconnected!' 10 | end 11 | 12 | private 13 | 14 | def set_authorization 15 | @authorization = current_user.authorizations.find(params[:id]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/users/omniauth_callbacks_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController 4 | def all 5 | user = User.from_omniauth(request.env['omniauth.auth'], current_user) 6 | 7 | if user.persisted? 8 | sign_in_and_redirect(user) 9 | else 10 | redirect_to root_path 11 | end 12 | end 13 | 14 | alias twitter all 15 | alias mastodon all 16 | 17 | def failure 18 | redirect_to root_path 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' } 5 | 6 | devise_scope :user do 7 | delete 'sign_out', to: 'devise/sessions#destroy', as: :destroy_user_session 8 | end 9 | 10 | resources :friends, only: :index do 11 | member do 12 | get :follow 13 | end 14 | end 15 | 16 | resource :account 17 | resources :authorizations, only: [:destroy] 18 | 19 | root to: 'home#index' 20 | end 21 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | .env 19 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /app/views/friends/_instances_grid.html.haml: -------------------------------------------------------------------------------- 1 | .grid 2 | - instances.each do |instance_info| 3 | = link_to "https://#{instance_info['uri']}/about", class: 'instance-card', style: "background-image: url(#{instance_info['thumbnail']})" do 4 | .info 5 | %span.title= instance_info['title'] 6 | %span.uri= instance_info['uri'] 7 | - if instance_info['stats'].is_a?(Hash) 8 | %span.users 9 | = surround '(', ')' do 10 | = number_with_delimiter instance_info['stats']['user_count'] 11 | = 'person'.pluralize(instance_info['stats']['user_count']) 12 | -------------------------------------------------------------------------------- /app/views/friends/_friends_grid.html.haml: -------------------------------------------------------------------------------- 1 | .grid 2 | - friends.each do |user| 3 | = link_to user.mastodon.info['url'] || user.mastodon.profile_url, class: 'user-card', title: "@#{user.twitter.display_name} on Twitter" do 4 | .avatar 5 | = image_tag user.mastodon.info['avatar'] unless user.mastodon.info['avatar'].blank? 6 | - if user.following 7 | .following-indicator= fa_icon('check') 8 | .name 9 | %span.display-name= user.mastodon.info['display_name'].presence || user.mastodon.info['username'] || user.mastodon.display_name 10 | %span.username= "@#{user.mastodon.uid}" 11 | -------------------------------------------------------------------------------- /app/views/accounts/show.html.haml: -------------------------------------------------------------------------------- 1 | .page-heading 2 | %h3 3 | Connections 4 | %small Manage your connected accounts 5 | 6 | %ul.connections 7 | - @authorizations.each do |authorization| 8 | %li 9 | .connection-brand{ class: authorization.provider } 10 | - if authorization.provider == 'twitter' 11 | = image_tag 'twitter-square.png', alt: 'Twitter' 12 | - else 13 | = image_tag 'logo.svg', alt: 'Mastodon' 14 | 15 | .connection-info 16 | %strong= authorization.display_name.presence || authorization.uid 17 | = fa_icon 'check' 18 | = link_to 'Disconnect', authorization_path(authorization), method: :delete, class: 'connection-disconnect-link' 19 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /app/models/authorization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Authorization < ApplicationRecord 4 | belongs_to :user, inverse_of: :authorizations, required: true 5 | 6 | default_scope { order('id asc') } 7 | 8 | def domain 9 | return unless provider == 'mastodon' 10 | uid.split('@').last 11 | end 12 | 13 | def info 14 | return @info if defined?(@info) 15 | 16 | if provider == 'mastodon' 17 | @info = Rails.cache.fetch("mastodon-user:#{uid}", expires_in: 1.day) do 18 | client = Mastodon::REST::Client.new(base_url: "https://#{domain}", bearer_token: token) 19 | client.verify_credentials.attributes 20 | end 21 | else 22 | @info = {} 23 | end 24 | rescue Mastodon::Error, HTTP::Error, OpenSSL::SSL::SSLError 25 | @info = {} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require font-awesome 15 | *= require_self 16 | */ 17 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | # require "action_cable/engine" 12 | require "sprockets/railtie" 13 | require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module MastodonBridge 20 | class Application < Rails::Application 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | config.active_job.queue_adapter = :sidekiq 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 2644fe140bec29a811c1fb15540758bef513c2042cc9f913ef8d1e1653285e09ea7202e29d5d091e46b83fdc787e1b3928a6a3d0f49758facf8744b1b19092bd 15 | 16 | test: 17 | secret_key_base: 3ec73923e86378bf74e0a1d332e08e808e9ff7eea94c3a596ed05c5b8ad45d680da4a6feb0c256f3fb47a135fb1b609a3f844a7f4881c11d46ccb448bfd77579 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Rails 5.0 release notes for more info on each option. 6 | 7 | # Enable per-form CSRF tokens. Previous versions had false. 8 | Rails.application.config.action_controller.per_form_csrf_tokens = true 9 | 10 | # Enable origin-checking CSRF mitigation. Previous versions had false. 11 | Rails.application.config.action_controller.forgery_protection_origin_check = true 12 | 13 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 14 | # Previous versions had false. 15 | ActiveSupport.to_time_preserves_timezone = true 16 | 17 | # Require `belongs_to` associations by default. Previous versions had false. 18 | Rails.application.config.active_record.belongs_to_required_by_default = true 19 | 20 | # Do not halt callback chains when a callback returns false. Previous versions had true. 21 | ActiveSupport.halt_callback_chains_on_return_false = false 22 | 23 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 24 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 25 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | ruby '>= 2.3.0', '< 2.5.0' 5 | 6 | gem 'rails', '~> 5.1.0' 7 | gem 'pg', '~> 0.20' 8 | gem 'puma', '~> 3.10' 9 | gem 'sass-rails', '~> 5.0' 10 | gem 'font-awesome-rails' 11 | gem 'jquery-rails' 12 | gem 'uglifier' 13 | 14 | gem 'bootsnap' 15 | gem 'mastodon-api', require: 'mastodon', git: 'https://github.com/tootsuite/mastodon-api' 16 | gem 'twitter', git: 'https://github.com/sferik/twitter' 17 | gem 'devise', '~> 4.3' 18 | gem 'omniauth-twitter' 19 | gem 'omniauth-mastodon', '>= 0.9.2' 20 | gem 'hamlit-rails', '~> 0.2' 21 | gem 'fast_blank', '~> 1.0' 22 | gem 'dotenv-rails', '~> 2.2' 23 | gem 'http' 24 | gem 'httplog', '~> 0.99' 25 | gem 'hiredis', '~> 0.6' 26 | gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis'] 27 | gem 'sidekiq', '~> 5.0' 28 | gem 'redis-rails', '~> 5.0' 29 | gem 'oj' 30 | 31 | group :development, :test do 32 | gem 'pry-rails', '~> 0.3' 33 | end 34 | 35 | group :development do 36 | gem 'listen', '~> 3.0.5' 37 | gem 'spring' 38 | gem 'spring-watcher-listen', '~> 2.0.0' 39 | gem 'better_errors', '~> 2.4' 40 | gem 'binding_of_caller', '~> 0.7' 41 | end 42 | 43 | group :production do 44 | gem 'lograge', '~> 0.5' 45 | gem 'rails_12factor' 46 | end 47 | -------------------------------------------------------------------------------- /app/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/models/mastodon_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MastodonClient < ApplicationRecord 4 | class << self 5 | def obtain!(domain, callback_url) 6 | new_client = Mastodon::REST::Client.new(base_url: "https://#{domain}").create_app('Mastodon Bridge', callback_url, 'read follow') 7 | client = self.new(domain: domain) 8 | 9 | client.client_id = new_client.client_id 10 | client.client_secret = new_client.client_secret 11 | 12 | client.save! 13 | client 14 | end 15 | end 16 | 17 | def client_token 18 | return attributes['client_token'] if attributes['client_token'].present? 19 | 20 | res = http_client.post("https://#{domain}/oauth/token", params: { 21 | grant_type: 'client_credentials', 22 | client_id: client_id, 23 | client_secret: client_secret, 24 | }) 25 | 26 | info = Oj.load(res.to_s, mode: :null) 27 | 28 | return if info.nil? 29 | 30 | update!(client_token: info['access_token']) 31 | info['access_token'] 32 | end 33 | 34 | def still_valid? 35 | return false if client_token.blank? 36 | 37 | res = http_client.get("https://#{domain}/api/v1/apps/verify_credentials", headers: { 'Authorization': "Bearer #{client_token}" }) 38 | res.code == 200 39 | end 40 | 41 | private 42 | 43 | def http_client 44 | HTTP.timeout(:per_operation, connect: 2, read: 5, write: 5) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/views/friends/index.html.haml: -------------------------------------------------------------------------------- 1 | .page-heading{ class: current_user.mastodon.nil? ? 'bottomless' : '' } 2 | %h3 3 | Your friends 4 | %small Here are your Twitter friends who are on Mastodon: 5 | 6 | - if current_user.mastodon.nil? 7 | .connect-prompt 8 | For your friends to find you as well, you still need to 9 | = link_to 'login via Mastodon', user_mastodon_omniauth_authorize_path 10 | 11 | - if @friends.empty? 12 | %p.empty-message Well, this is unfortunate. Looks like none of your Twitter friends are on Mastodon yet. Or maybe they are, but haven't signed in to this bridge service. 13 | 14 | - if current_user.mastodon.nil? 15 | %p.empty-message Here are some instances you could choose to make your account on: 16 | - else 17 | %p.empty-message Here are some instances you could recommend to your friends: 18 | - else 19 | = render 'friends_grid', friends: @friends 20 | 21 | - if @next_page || @page > 1 22 | .pagination 23 | - if @page > 1 24 | = link_to friends_path(page: @page - 1) do 25 | = fa_icon('chevron-left fw') 26 | Previous 27 | - if @next_page 28 | = link_to friends_path(page: @next_page) do 29 | Next 30 | = fa_icon('chevron-right fw') 31 | 32 | .page-heading 33 | %h3 34 | Your friends' instances 35 | %small Here are the instances your friends are using: 36 | 37 | = render 'instances_grid', instances: @top_instances 38 | -------------------------------------------------------------------------------- /app/views/home/index.html.haml: -------------------------------------------------------------------------------- 1 | .page-heading 2 | %h3 3 | Find your friends on Mastodon 4 | %small Login via Twitter to get started 5 | 6 | = link_to 'https://joinmastodon.org', class: 'hero-link' do 7 | = image_tag 'preview.jpg', alt: 'Elephant friend, paperplanes and a trombone with the Mastodon logo in the center' 8 | 9 | .login-buttons 10 | = content_tag twitter? ? :div : :a, href: user_twitter_omniauth_authorize_path, class: 'twitter' do 11 | .login-brand= image_tag 'twitter-square.png', alt: 'Twitter' 12 | .login-content 13 | .login-numbers 14 | %strong= number_with_delimiter @twitter_count 15 | connected 16 | - if twitter? 17 | .login-name 18 | %strong= current_user.twitter.display_name.presence || current_user.twitter.uid 19 | = fa_icon 'check' 20 | - else 21 | %span.login-btn Connect Twitter 22 | 23 | = content_tag mastodon? ? :div : :a, href: user_mastodon_omniauth_authorize_path, class: 'mastodon' do 24 | .login-brand= image_tag 'logo.svg', alt: 'Mastodon' 25 | .login-content 26 | .login-numbers 27 | %strong= number_with_delimiter @mastodon_count 28 | connected 29 | - if mastodon? 30 | .login-name 31 | %strong= current_user.mastodon.display_name.presence || current_user.mastodon.uid 32 | = fa_icon 'check' 33 | - else 34 | %span.login-btn Connect Mastodon 35 | 36 | %p 37 | This website uses a database of Twitter users and Mastodon users who signed in here to match them together across multiple Mastodon instances. Are your friends among them? 38 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | devise :omniauthable, omniauth_providers: [:twitter, :mastodon] 5 | 6 | has_many :authorizations 7 | 8 | has_one :twitter, -> { where(provider: 'twitter') }, class_name: 'Authorization' 9 | has_one :mastodon, -> { where(provider: 'mastodon') }, class_name: 'Authorization' 10 | 11 | attr_accessor :relative_account_id, :following 12 | 13 | def twitter_client 14 | return if twitter.nil? 15 | 16 | @twitter_client ||= Twitter::REST::Client.new do |config| 17 | config.consumer_key = ENV['TWITTER_CLIENT_ID'] 18 | config.consumer_secret = ENV['TWITTER_CLIENT_SECRET'] 19 | config.access_token = twitter.token 20 | config.access_token_secret = twitter.secret 21 | end 22 | end 23 | 24 | def mastodon_client 25 | return if mastodon.nil? 26 | 27 | @mastodon_client ||= Mastodon::REST::Client.new(base_url: "https://#{mastodon.domain}", bearer_token: mastodon.token) 28 | end 29 | 30 | class << self 31 | def from_omniauth(auth, current_user) 32 | authorization = Authorization.where(provider: auth.provider, uid: auth.uid.to_s).first_or_initialize(provider: auth.provider, uid: auth.uid.to_s) 33 | user = current_user || authorization.user || User.new 34 | authorization.user = user 35 | authorization.token = auth.credentials.token 36 | authorization.secret = auth.credentials.secret 37 | 38 | if auth.provider == 'twitter' 39 | authorization.profile_url = auth.info.urls['Twitter'] 40 | authorization.display_name = auth.info.nickname 41 | elsif auth.provider == 'mastodon' 42 | authorization.profile_url = auth.info.urls['Profile'] 43 | authorization.display_name = auth.info.nickname 44 | end 45 | 46 | authorization.save 47 | authorization.user 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20180110004149) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "authorizations", id: :serial, force: :cascade do |t| 19 | t.string "provider" 20 | t.string "uid" 21 | t.integer "user_id" 22 | t.string "token" 23 | t.string "secret" 24 | t.datetime "created_at", null: false 25 | t.datetime "updated_at", null: false 26 | t.string "profile_url", default: "", null: false 27 | t.string "display_name", default: "", null: false 28 | t.index ["provider", "uid"], name: "index_authorizations_on_provider_and_uid", unique: true 29 | end 30 | 31 | create_table "mastodon_clients", id: :serial, force: :cascade do |t| 32 | t.string "domain" 33 | t.string "client_id" 34 | t.string "client_secret" 35 | t.datetime "created_at", null: false 36 | t.datetime "updated_at", null: false 37 | t.string "client_token" 38 | t.index ["domain"], name: "index_mastodon_clients_on_domain", unique: true 39 | end 40 | 41 | create_table "users", id: :serial, force: :cascade do |t| 42 | t.datetime "created_at", null: false 43 | t.datetime "updated_at", null: false 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ 5 | %title Find your Twitter friends on Mastodon - Mastodon Bridge 6 | 7 | = csrf_meta_tags 8 | = stylesheet_link_tag 'application', media: 'all' 9 | = javascript_include_tag 'application' 10 | 11 | %meta{ property: 'og:type', content: 'article' }/ 12 | %meta{ property: 'og:title', content: 'Find your Twitter friends on Mastodon' }/ 13 | %meta{ property: 'og:description', content: 'This bridge tool matches you with your friends in the decentralized Mastodon network' }/ 14 | %meta{ property: 'og:image', content: image_url('preview.jpg') }/ 15 | %meta{ property: 'og:url', content: root_url }/ 16 | %meta{ property: 'og:site_name', content: 'Mastodon Bridge' }/ 17 | 18 | %meta{ name: 'twitter:card', content: 'summary' }/ 19 | %meta{ name: 'twitter:site', content: '@MastodonProject' }/ 20 | %body 21 | .header 22 | %ul.left 23 | %li 24 | = link_to root_path, class: 'brand' do 25 | = image_tag 'logo_full.svg', alt: 'Mastodon Bridge' 26 | 27 | %ul.right 28 | - if twitter? 29 | %li= link_to 'Friends', friends_path 30 | 31 | - if user_signed_in? 32 | %li= link_to 'Account', account_path 33 | %li= link_to 'Logout', destroy_user_session_path, method: :delete 34 | 35 | - if flash[:notice] 36 | .flash-message 37 | %strong Success! 38 | = flash[:notice] 39 | - if flash[:alert] 40 | .flash-message 41 | %strong Oops! 42 | = flash[:alert] 43 | 44 | = yield 45 | 46 | %hr/ 47 | 48 | %p.footer 49 | This website is run by 50 | %a{:href => "https://mastodon.social/@Mastodon"} @Mastodon@mastodon.social 51 | \/ 52 | = succeed "." do 53 | %a{:href => "https://twitter.com/MastodonProject"} @MastodonProject 54 | %a{:href => "https://github.com/tootsuite/mastodon-bridge", :target => "_blank"} Source code 55 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :redis_store, ENV['REDIS_URL'] 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 47 | 48 | # Raises error for missing translations 49 | # config.action_view.raise_on_missing_translations = true 50 | 51 | # Use an evented file watcher to asynchronously detect changes in source code, 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | end 55 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | on_worker_boot do 43 | ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.3 3 | Exclude: 4 | - 'spec/**/*' 5 | - 'db/**/*' 6 | - 'app/views/**/*' 7 | - 'config/**/*' 8 | - 'bin/*' 9 | - 'Rakefile' 10 | - 'node_modules/**/*' 11 | - 'Vagrantfile' 12 | - 'vendor/**/*' 13 | 14 | Bundler/OrderedGems: 15 | Enabled: false 16 | 17 | Layout/AccessModifierIndentation: 18 | EnforcedStyle: indent 19 | 20 | Layout/EmptyLineAfterMagicComment: 21 | Enabled: false 22 | 23 | Layout/SpaceInsideHashLiteralBraces: 24 | EnforcedStyle: space 25 | 26 | Metrics/AbcSize: 27 | Max: 100 28 | 29 | Metrics/BlockLength: 30 | Max: 35 31 | Exclude: 32 | - 'lib/tasks/**/*' 33 | 34 | Metrics/BlockNesting: 35 | Max: 3 36 | 37 | Metrics/ClassLength: 38 | CountComments: false 39 | Max: 300 40 | 41 | Metrics/CyclomaticComplexity: 42 | Max: 25 43 | 44 | Metrics/LineLength: 45 | AllowURI: true 46 | Enabled: false 47 | 48 | Metrics/MethodLength: 49 | CountComments: false 50 | Max: 55 51 | 52 | Metrics/ModuleLength: 53 | CountComments: false 54 | Max: 200 55 | 56 | Metrics/ParameterLists: 57 | Max: 5 58 | CountKeywordArgs: true 59 | 60 | Metrics/PerceivedComplexity: 61 | Max: 20 62 | 63 | Rails: 64 | Enabled: true 65 | 66 | Rails/HasAndBelongsToMany: 67 | Enabled: false 68 | 69 | Rails/SkipsModelValidations: 70 | Enabled: false 71 | 72 | Style/ClassAndModuleChildren: 73 | Enabled: false 74 | 75 | Style/CollectionMethods: 76 | Enabled: true 77 | PreferredMethods: 78 | find_all: 'select' 79 | 80 | Style/Documentation: 81 | Enabled: false 82 | 83 | Style/DoubleNegation: 84 | Enabled: true 85 | 86 | Style/FrozenStringLiteralComment: 87 | Enabled: true 88 | 89 | Style/GuardClause: 90 | Enabled: false 91 | 92 | Style/Lambda: 93 | Enabled: false 94 | 95 | Style/PercentLiteralDelimiters: 96 | PreferredDelimiters: 97 | '%i': '()' 98 | '%w': '()' 99 | 100 | Style/PerlBackrefs: 101 | AutoCorrect: false 102 | 103 | Style/RegexpLiteral: 104 | Enabled: false 105 | 106 | Style/SymbolArray: 107 | Enabled: false 108 | 109 | Style/TrailingCommaInLiteral: 110 | EnforcedStyleForMultiline: 'comma' 111 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.1 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see rails configuration guide 21 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: mastodon-bridge_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user that initialized the database. 32 | #username: mastodon-bridge 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: mastodon-bridge_test 61 | 62 | # As with config/secrets.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password as a unix environment variable when you boot 67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 68 | # for a full rundown on how to provide these environment variables in a 69 | # production deployment. 70 | # 71 | # On Heroku and other platform providers, you may have a full connection URL 72 | # available as an environment variable. For example: 73 | # 74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 75 | # 76 | # You can use this database configuration with: 77 | # 78 | # production: 79 | # url: <%= ENV['DATABASE_URL'] %> 80 | # 81 | production: 82 | <<: *default 83 | database: mastodon-bridge_production 84 | username: mastodon-bridge 85 | password: <%= ENV['MASTODON-BRIDGE_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | config.lograge.enabled = true 4 | 5 | # Code is not reloaded between requests. 6 | config.cache_classes = true 7 | 8 | # Eager load code on boot. This eager loads most of Rails and 9 | # your application in memory, allowing both threaded web servers 10 | # and those relying on copy on write to perform better. 11 | # Rake tasks automatically ignore this option for performance. 12 | config.eager_load = true 13 | 14 | # Full error reports are disabled and caching is turned on. 15 | config.consider_all_requests_local = false 16 | config.action_controller.perform_caching = true 17 | 18 | # Disable serving static files from the `/public` folder by default since 19 | # Apache or NGINX already handles this. 20 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 21 | 22 | # Compress JavaScripts and CSS. 23 | config.assets.js_compressor = :uglifier 24 | # config.assets.css_compressor = :sass 25 | 26 | # Do not fallback to assets pipeline if a precompiled asset is missed. 27 | config.assets.compile = false 28 | 29 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 30 | 31 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 32 | # config.action_controller.asset_host = 'http://assets.example.com' 33 | 34 | # Specifies the header that your server uses for sending files. 35 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 36 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 37 | 38 | 39 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 40 | # config.force_ssl = true 41 | 42 | # Use the lowest log level to ensure availability of diagnostic information 43 | # when problems arise. 44 | config.log_level = :debug 45 | 46 | # Prepend all log lines with the following tags. 47 | config.log_tags = [ :request_id ] 48 | 49 | # Use a different cache store in production. 50 | config.cache_store = :redis_store, ENV['REDIS_URL'] 51 | 52 | # Use a real queuing backend for Active Job (and separate queues per environment) 53 | # config.active_job.queue_adapter = :resque 54 | # config.active_job.queue_name_prefix = "mastodon-bridge_#{Rails.env}" 55 | config.action_mailer.perform_caching = false 56 | 57 | # Ignore bad email addresses and do not raise email delivery errors. 58 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 59 | # config.action_mailer.raise_delivery_errors = false 60 | 61 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 62 | # the I18n.default_locale when a translation cannot be found). 63 | config.i18n.fallbacks = true 64 | 65 | # Send deprecation notices to registered listeners. 66 | config.active_support.deprecation = :notify 67 | 68 | # Use default logging formatter so that PID and timestamp are not suppressed. 69 | config.log_formatter = ::Logger::Formatter.new 70 | 71 | # Use a different logger for distributed setups. 72 | # require 'syslog/logger' 73 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 74 | 75 | if ENV["RAILS_LOG_TO_STDOUT"].present? 76 | logger = ActiveSupport::Logger.new(STDOUT) 77 | logger.formatter = config.log_formatter 78 | config.logger = ActiveSupport::TaggedLogging.new(logger) 79 | end 80 | 81 | # Do not dump schema after migrations. 82 | config.active_record.dump_schema_after_migration = false 83 | end 84 | -------------------------------------------------------------------------------- /app/controllers/friends_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'twitter' 4 | 5 | class FriendsController < ApplicationController 6 | before_action :authenticate_user! 7 | before_action :set_page 8 | before_action :set_friends 9 | before_action :set_top_instances 10 | before_action :set_next_page 11 | before_action :paginate_friends 12 | before_action :set_relationships, if: -> { current_user&.mastodon } 13 | 14 | rescue_from Twitter::Error do |e| 15 | redirect_to root_path, alert: "Twitter error: #{e}" 16 | end 17 | 18 | PER_PAGE_FRIENDS = 20 19 | MAX_INSTANCES = 20 20 | MIN_INSTANCES = 4 21 | 22 | def index; end 23 | 24 | private 25 | 26 | def set_page 27 | @page = (params['page'] || 1).to_i 28 | end 29 | 30 | def set_next_page 31 | @next_page = @friends.size > (@page * PER_PAGE_FRIENDS) ? @page + 1 : nil 32 | end 33 | 34 | def set_friends 35 | @friends = User.where(id: Authorization.where(provider: :twitter, uid: twitter_friend_ids).map(&:user_id)) 36 | .includes(:twitter, :mastodon) 37 | .reject { |user| user.mastodon.nil? } 38 | end 39 | 40 | def set_top_instances 41 | @top_instances = friends_domains.map { |k, _| fetch_instance_info(k) }.compact 42 | end 43 | 44 | def paginate_friends 45 | @friends = @friends.slice([(@page - 1) * PER_PAGE_FRIENDS, @friends.size].min, PER_PAGE_FRIENDS) 46 | .map { |user| fetch_account_id(user) } 47 | end 48 | 49 | def friends_domains 50 | return default_domains.sample(MIN_INSTANCES) if @friends.empty? 51 | 52 | @friends.collect { |user| user&.mastodon&.uid } 53 | .compact 54 | .map { |uid| uid.split('@').last } 55 | .inject(Hash.new(0)) { |h, k| h[k] += 1; h } 56 | .sort_by { |k, v| -v } 57 | .take(MAX_INSTANCES) 58 | end 59 | 60 | def default_domains 61 | %w( 62 | octodon.social 63 | mastodon.art 64 | niu.moe 65 | todon.nl 66 | soc.ialis.me 67 | scifi.fyi 68 | hostux.social 69 | mstdn.maud.io 70 | mastodon.sdf.org 71 | x0r.be 72 | toot.cafe 73 | ) 74 | end 75 | 76 | def twitter_friend_ids 77 | Rails.cache.fetch("#{current_user.id}/twitter-friends", expires_in: 15.minutes) { current_user.twitter_client.friend_ids.to_a } 78 | end 79 | 80 | def fetch_instance_info(host) 81 | Rails.cache.fetch("instance:#{host}", expires_in: 1.week) { Oj.load(HTTP.get("https://#{host}/api/v1/instance").to_s, mode: :strict) } 82 | rescue HTTP::Error, OpenSSL::SSL::SSLError, Oj::ParseError 83 | nil 84 | end 85 | 86 | def fetch_account_id(user) 87 | user.tap do |user| 88 | next if current_user.mastodon.nil? 89 | 90 | begin 91 | user.relative_account_id = Rails.cache.fetch("#{current_user.id}/#{current_user.mastodon.domain}/#{user.mastodon.uid}", expires_in: 1.week) do 92 | account, _ = current_user.mastodon_client.perform_request(:get, '/api/v1/accounts/search', q: user.mastodon.uid, resolve: 'true', limit: 1) 93 | next if account.nil? 94 | account['id'] 95 | end 96 | rescue Mastodon::Error, HTTP::Error, OpenSSL::SSL::SSLError 97 | next 98 | end 99 | end 100 | end 101 | 102 | def set_relationships 103 | account_map = @friends.map { |user| [user.relative_account_id, user] }.to_h 104 | account_ids = @friends.collect { |user| user.relative_account_id }.compact 105 | param_str = account_ids.map { |id| "id[]=#{id}" }.join('&') 106 | 107 | current_user.mastodon_client.perform_request(:get, "/api/v1/accounts/relationships?#{param_str}").each do |relationship| 108 | account_map[relationship['id']].following = relationship['following'] 109 | end 110 | rescue Mastodon::Error, HTTP::Error, OpenSSL::SSL::SSLError 111 | nil 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | password_change: 27 | subject: "Password Changed" 28 | omniauth_callbacks: 29 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 30 | success: "Successfully authenticated from %{kind} account." 31 | passwords: 32 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 33 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 34 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 35 | updated: "Your password has been changed successfully. You are now signed in." 36 | updated_not_active: "Your password has been changed successfully." 37 | registrations: 38 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 39 | signed_up: "Welcome! You have signed up successfully." 40 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 41 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 42 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 43 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." 44 | updated: "Your account has been updated successfully." 45 | sessions: 46 | signed_in: "Signed in successfully." 47 | signed_out: "Signed out successfully." 48 | already_signed_out: "Signed out successfully." 49 | unlocks: 50 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 51 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 52 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 53 | errors: 54 | messages: 55 | already_confirmed: "was already confirmed, please try signing in" 56 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 57 | expired: "has expired, please request a new one" 58 | not_found: "not found" 59 | not_locked: "was not locked" 60 | not_saved: 61 | one: "1 error prohibited this %{resource} from being saved:" 62 | other: "%{count} errors prohibited this %{resource} from being saved:" 63 | -------------------------------------------------------------------------------- /app/assets/images/logo_full.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/sferik/twitter 3 | revision: d6c5100447e2c12d83bae26810632081ac0b69f5 4 | specs: 5 | twitter (6.2.0) 6 | addressable (~> 2.3) 7 | buftok (~> 0.2.0) 8 | equalizer (~> 0.0.11) 9 | http (~> 3.0) 10 | http-form_data (~> 2.0) 11 | http_parser.rb (~> 0.6.0) 12 | memoizable (~> 0.4.0) 13 | multipart-post (~> 2.0) 14 | naught (~> 1.0) 15 | simple_oauth (~> 0.3.0) 16 | 17 | GIT 18 | remote: https://github.com/tootsuite/mastodon-api 19 | revision: 6557c5cc580f611c75da5f77280ec1dc571d08e0 20 | specs: 21 | mastodon-api (1.2.0) 22 | addressable (~> 2.5) 23 | buftok 24 | http (~> 3.0) 25 | oj (~> 3.3) 26 | 27 | GEM 28 | remote: https://rubygems.org/ 29 | specs: 30 | actioncable (5.1.2) 31 | actionpack (= 5.1.2) 32 | nio4r (~> 2.0) 33 | websocket-driver (~> 0.6.1) 34 | actionmailer (5.1.2) 35 | actionpack (= 5.1.2) 36 | actionview (= 5.1.2) 37 | activejob (= 5.1.2) 38 | mail (~> 2.5, >= 2.5.4) 39 | rails-dom-testing (~> 2.0) 40 | actionpack (5.1.2) 41 | actionview (= 5.1.2) 42 | activesupport (= 5.1.2) 43 | rack (~> 2.0) 44 | rack-test (~> 0.6.3) 45 | rails-dom-testing (~> 2.0) 46 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 47 | actionview (5.1.2) 48 | activesupport (= 5.1.2) 49 | builder (~> 3.1) 50 | erubi (~> 1.4) 51 | rails-dom-testing (~> 2.0) 52 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 53 | activejob (5.1.2) 54 | activesupport (= 5.1.2) 55 | globalid (>= 0.3.6) 56 | activemodel (5.1.2) 57 | activesupport (= 5.1.2) 58 | activerecord (5.1.2) 59 | activemodel (= 5.1.2) 60 | activesupport (= 5.1.2) 61 | arel (~> 8.0) 62 | activesupport (5.1.2) 63 | concurrent-ruby (~> 1.0, >= 1.0.2) 64 | i18n (~> 0.7) 65 | minitest (~> 5.1) 66 | tzinfo (~> 1.1) 67 | addressable (2.5.2) 68 | public_suffix (>= 2.0.2, < 4.0) 69 | arel (8.0.0) 70 | bcrypt (3.1.11) 71 | better_errors (2.4.0) 72 | coderay (>= 1.0.0) 73 | erubi (>= 1.0.0) 74 | rack (>= 0.9.0) 75 | binding_of_caller (0.7.3) 76 | debug_inspector (>= 0.0.1) 77 | bootsnap (1.1.1) 78 | msgpack (~> 1.0) 79 | buftok (0.2.0) 80 | builder (3.2.3) 81 | coderay (1.1.2) 82 | colorize (0.8.1) 83 | concurrent-ruby (1.0.5) 84 | connection_pool (2.2.1) 85 | crass (1.0.3) 86 | debug_inspector (0.0.3) 87 | devise (4.4.0) 88 | bcrypt (~> 3.0) 89 | orm_adapter (~> 0.1) 90 | railties (>= 4.1.0, < 5.2) 91 | responders 92 | warden (~> 1.2.3) 93 | domain_name (0.5.20170404) 94 | unf (>= 0.0.5, < 1.0.0) 95 | dotenv (2.2.1) 96 | dotenv-rails (2.2.1) 97 | dotenv (= 2.2.1) 98 | railties (>= 3.2, < 5.2) 99 | equalizer (0.0.11) 100 | erubi (1.7.0) 101 | execjs (2.7.0) 102 | faraday (0.12.2) 103 | multipart-post (>= 1.2, < 3) 104 | fast_blank (1.0.0) 105 | ffi (1.9.18) 106 | font-awesome-rails (4.7.0.2) 107 | railties (>= 3.2, < 5.2) 108 | globalid (0.4.0) 109 | activesupport (>= 4.2.0) 110 | hamlit (2.8.4) 111 | temple (>= 0.8.0) 112 | thor 113 | tilt 114 | hamlit-rails (0.2.0) 115 | actionpack (>= 4.0.1) 116 | activesupport (>= 4.0.1) 117 | hamlit (>= 1.2.0) 118 | railties (>= 4.0.1) 119 | hashie (3.5.6) 120 | hiredis (0.6.1) 121 | http (3.0.0) 122 | addressable (~> 2.3) 123 | http-cookie (~> 1.0) 124 | http-form_data (>= 2.0.0.pre.pre2, < 3) 125 | http_parser.rb (~> 0.6.0) 126 | http-cookie (1.0.3) 127 | domain_name (~> 0.5) 128 | http-form_data (2.0.0) 129 | http_parser.rb (0.6.0) 130 | httplog (0.99.7) 131 | colorize 132 | rack 133 | i18n (0.9.1) 134 | concurrent-ruby (~> 1.0) 135 | jquery-rails (4.3.1) 136 | rails-dom-testing (>= 1, < 3) 137 | railties (>= 4.2.0) 138 | thor (>= 0.14, < 2.0) 139 | jwt (1.5.6) 140 | listen (3.0.8) 141 | rb-fsevent (~> 0.9, >= 0.9.4) 142 | rb-inotify (~> 0.9, >= 0.9.7) 143 | lograge (0.5.1) 144 | actionpack (>= 4, < 5.2) 145 | activesupport (>= 4, < 5.2) 146 | railties (>= 4, < 5.2) 147 | loofah (2.1.1) 148 | crass (~> 1.0.2) 149 | nokogiri (>= 1.5.9) 150 | mail (2.6.6) 151 | mime-types (>= 1.16, < 4) 152 | memoizable (0.4.2) 153 | thread_safe (~> 0.3, >= 0.3.1) 154 | method_source (0.8.2) 155 | mime-types (3.1) 156 | mime-types-data (~> 3.2015) 157 | mime-types-data (3.2016.0521) 158 | mini_portile2 (2.3.0) 159 | minitest (5.11.1) 160 | msgpack (1.1.0) 161 | multi_json (1.12.1) 162 | multi_xml (0.6.0) 163 | multipart-post (2.0.0) 164 | naught (1.1.0) 165 | nio4r (2.1.0) 166 | nokogiri (1.8.1) 167 | mini_portile2 (~> 2.3.0) 168 | oauth (0.5.3) 169 | oauth2 (1.4.0) 170 | faraday (>= 0.8, < 0.13) 171 | jwt (~> 1.0) 172 | multi_json (~> 1.3) 173 | multi_xml (~> 0.5) 174 | rack (>= 1.2, < 3) 175 | oj (3.3.10) 176 | omniauth (1.6.1) 177 | hashie (>= 3.4.6, < 3.6.0) 178 | rack (>= 1.6.2, < 3) 179 | omniauth-mastodon (0.9.3) 180 | omniauth (~> 1.0) 181 | omniauth-oauth2 (~> 1.1) 182 | omniauth-oauth (1.1.0) 183 | oauth 184 | omniauth (~> 1.0) 185 | omniauth-oauth2 (1.4.0) 186 | oauth2 (~> 1.0) 187 | omniauth (~> 1.2) 188 | omniauth-twitter (1.4.0) 189 | omniauth-oauth (~> 1.1) 190 | rack 191 | orm_adapter (0.5.0) 192 | pg (0.21.0) 193 | pry (0.11.0) 194 | coderay (~> 1.1.0) 195 | method_source (~> 0.8.1) 196 | pry-rails (0.3.6) 197 | pry (>= 0.10.4) 198 | public_suffix (3.0.1) 199 | puma (3.11.0) 200 | rack (2.0.3) 201 | rack-protection (2.0.0) 202 | rack 203 | rack-test (0.6.3) 204 | rack (>= 1.0) 205 | rails (5.1.2) 206 | actioncable (= 5.1.2) 207 | actionmailer (= 5.1.2) 208 | actionpack (= 5.1.2) 209 | actionview (= 5.1.2) 210 | activejob (= 5.1.2) 211 | activemodel (= 5.1.2) 212 | activerecord (= 5.1.2) 213 | activesupport (= 5.1.2) 214 | bundler (>= 1.3.0, < 2.0) 215 | railties (= 5.1.2) 216 | sprockets-rails (>= 2.0.0) 217 | rails-dom-testing (2.0.3) 218 | activesupport (>= 4.2.0) 219 | nokogiri (>= 1.6) 220 | rails-html-sanitizer (1.0.3) 221 | loofah (~> 2.0) 222 | rails_12factor (0.0.3) 223 | rails_serve_static_assets 224 | rails_stdout_logging 225 | rails_serve_static_assets (0.0.5) 226 | rails_stdout_logging (0.0.5) 227 | railties (5.1.2) 228 | actionpack (= 5.1.2) 229 | activesupport (= 5.1.2) 230 | method_source 231 | rake (>= 0.8.7) 232 | thor (>= 0.18.1, < 2.0) 233 | rake (12.3.0) 234 | rb-fsevent (0.10.2) 235 | rb-inotify (0.9.10) 236 | ffi (>= 0.5.0, < 2) 237 | redis (3.3.5) 238 | redis-actionpack (5.0.2) 239 | actionpack (>= 4.0, < 6) 240 | redis-rack (>= 1, < 3) 241 | redis-store (>= 1.1.0, < 2) 242 | redis-activesupport (5.0.4) 243 | activesupport (>= 3, < 6) 244 | redis-store (>= 1.3, < 2) 245 | redis-rack (2.0.4) 246 | rack (>= 1.5, < 3) 247 | redis-store (>= 1.2, < 2) 248 | redis-rails (5.0.2) 249 | redis-actionpack (>= 5.0, < 6) 250 | redis-activesupport (>= 5.0, < 6) 251 | redis-store (>= 1.2, < 2) 252 | redis-store (1.4.1) 253 | redis (>= 2.2, < 5) 254 | responders (2.4.0) 255 | actionpack (>= 4.2.0, < 5.3) 256 | railties (>= 4.2.0, < 5.3) 257 | sass (3.5.1) 258 | sass-listen (~> 4.0.0) 259 | sass-listen (4.0.0) 260 | rb-fsevent (~> 0.9, >= 0.9.4) 261 | rb-inotify (~> 0.9, >= 0.9.7) 262 | sass-rails (5.0.6) 263 | railties (>= 4.0.0, < 6) 264 | sass (~> 3.1) 265 | sprockets (>= 2.8, < 4.0) 266 | sprockets-rails (>= 2.0, < 4.0) 267 | tilt (>= 1.1, < 3) 268 | sidekiq (5.0.5) 269 | concurrent-ruby (~> 1.0) 270 | connection_pool (~> 2.2, >= 2.2.0) 271 | rack-protection (>= 1.5.0) 272 | redis (>= 3.3.4, < 5) 273 | simple_oauth (0.3.1) 274 | spring (2.0.2) 275 | activesupport (>= 4.2) 276 | spring-watcher-listen (2.0.1) 277 | listen (>= 2.7, < 4.0) 278 | spring (>= 1.2, < 3.0) 279 | sprockets (3.7.1) 280 | concurrent-ruby (~> 1.0) 281 | rack (> 1, < 3) 282 | sprockets-rails (3.2.0) 283 | actionpack (>= 4.0) 284 | activesupport (>= 4.0) 285 | sprockets (>= 3.0.0) 286 | temple (0.8.0) 287 | thor (0.20.0) 288 | thread_safe (0.3.6) 289 | tilt (2.0.8) 290 | tzinfo (1.2.4) 291 | thread_safe (~> 0.1) 292 | uglifier (3.2.0) 293 | execjs (>= 0.3.0, < 3) 294 | unf (0.1.4) 295 | unf_ext 296 | unf_ext (0.0.7.4) 297 | warden (1.2.7) 298 | rack (>= 1.0) 299 | websocket-driver (0.6.5) 300 | websocket-extensions (>= 0.1.0) 301 | websocket-extensions (0.1.2) 302 | 303 | PLATFORMS 304 | ruby 305 | 306 | DEPENDENCIES 307 | better_errors (~> 2.4) 308 | binding_of_caller (~> 0.7) 309 | bootsnap 310 | devise (~> 4.3) 311 | dotenv-rails (~> 2.2) 312 | fast_blank (~> 1.0) 313 | font-awesome-rails 314 | hamlit-rails (~> 0.2) 315 | hiredis (~> 0.6) 316 | http 317 | httplog (~> 0.99) 318 | jquery-rails 319 | listen (~> 3.0.5) 320 | lograge (~> 0.5) 321 | mastodon-api! 322 | oj 323 | omniauth-mastodon (>= 0.9.2) 324 | omniauth-twitter 325 | pg (~> 0.20) 326 | pry-rails (~> 0.3) 327 | puma (~> 3.10) 328 | rails (~> 5.1.0) 329 | rails_12factor 330 | redis (~> 3.3) 331 | redis-rails (~> 5.0) 332 | sass-rails (~> 5.0) 333 | sidekiq (~> 5.0) 334 | spring 335 | spring-watcher-listen (~> 2.0.0) 336 | twitter! 337 | uglifier 338 | 339 | RUBY VERSION 340 | ruby 2.4.1p111 341 | 342 | BUNDLED WITH 343 | 1.16.1 344 | -------------------------------------------------------------------------------- /app/assets/stylesheets/home.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Quando|Judson|Montserrat:500|Roboto:400,500'); 2 | 3 | $white: #fff !default; // color5 4 | $lightest: #d9e1e8 !default; // color2 5 | $lighter: #9baec8 !default; // color3 6 | $darkest: #1F232B !default; // color1 7 | $black: #000 !default; // color8 8 | $darker: lighten($darkest, 30%); 9 | 10 | $vibrant: #2b90d9 !default; // color4 11 | $error: #df405a !default; // color6 12 | $success: #79bd9a !default; // color7 13 | 14 | $transition-in: 100ms linear; 15 | $transition-out: 250ms linear; 16 | 17 | /* http://meyerweb.com/eric/tools/css/reset/ 18 | v2.0 | 20110126 19 | License: none (public domain) 20 | */ 21 | 22 | html, body, div, span, applet, object, iframe, 23 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 24 | a, abbr, acronym, address, big, cite, code, 25 | del, dfn, em, img, ins, kbd, q, s, samp, 26 | small, strike, strong, sub, sup, tt, var, 27 | b, u, i, center, 28 | dl, dt, dd, ol, ul, li, 29 | fieldset, form, label, legend, 30 | table, caption, tbody, tfoot, thead, tr, th, td, 31 | article, aside, canvas, details, embed, 32 | figure, figcaption, footer, header, hgroup, 33 | menu, nav, output, ruby, section, summary, 34 | time, mark, audio, video { 35 | margin: 0; 36 | padding: 0; 37 | border: 0; 38 | font-size: 100%; 39 | font: inherit; 40 | vertical-align: baseline; 41 | } 42 | /* HTML5 display-role reset for older browsers */ 43 | article, aside, details, figcaption, figure, 44 | footer, header, hgroup, menu, nav, section { 45 | display: block; 46 | } 47 | body { 48 | line-height: 1; 49 | } 50 | ol, ul { 51 | list-style: none; 52 | } 53 | blockquote, q { 54 | quotes: none; 55 | } 56 | blockquote:before, blockquote:after, 57 | q:before, q:after { 58 | content: ''; 59 | content: none; 60 | } 61 | table { 62 | border-collapse: collapse; 63 | border-spacing: 0; 64 | } 65 | 66 | body { 67 | background: #282c37; 68 | color: #9baec8; 69 | font-family: 'Roboto', sans-serif; 70 | font-size: 14px; 71 | line-height: 21px; 72 | font-weight: 400; 73 | max-width: 600px; 74 | margin: 0 auto; 75 | padding: 30px 10px; 76 | } 77 | 78 | a { 79 | color: lighten(#2b90d9, 4%); 80 | 81 | &:hover, &:focus, &:active { 82 | color: lighten(#2b90d9, 8%); 83 | } 84 | } 85 | 86 | hr { 87 | border: 0; 88 | background: none; 89 | border-bottom: 1px solid lighten(#282c37, 8%); 90 | margin: 30px 0; 91 | } 92 | 93 | .toot-friend img { 94 | display: block; 95 | margin: 0 auto; 96 | } 97 | 98 | p.lead { 99 | font-size: 16px; 100 | font-weight: 400; 101 | color: #fff; 102 | margin-bottom: 20px; 103 | } 104 | 105 | .flash-message { 106 | background: darken(#282c37, 8%); 107 | color: #9baec8; 108 | border-radius: 4px; 109 | padding: 15px 10px; 110 | margin-bottom: 30px; 111 | box-shadow: 0 0 5px rgba(#000, 0.2); 112 | text-align: center; 113 | 114 | strong { 115 | font-weight: 500; 116 | } 117 | } 118 | 119 | .buttons { 120 | list-style: none; 121 | display: flex; 122 | align-items: center; 123 | justify-content: center; 124 | margin-bottom: 20px; 125 | 126 | li { 127 | display: block; 128 | margin: 0 5px; 129 | } 130 | } 131 | 132 | .button, a.button { 133 | background-color: darken(#2b90d9, 3%); 134 | font-family: inherit; 135 | display: inline-block; 136 | position: relative; 137 | box-sizing: border-box; 138 | text-align: center; 139 | border: 10px none; 140 | color: #fff; 141 | font-size: 14px; 142 | font-weight: 500; 143 | letter-spacing: 0; 144 | text-transform: uppercase; 145 | padding: 0 16px; 146 | height: 36px; 147 | cursor: pointer; 148 | line-height: 36px; 149 | border-radius: 4px; 150 | text-decoration: none; 151 | transition: all 100ms ease-in; 152 | 153 | &:hover, &:active, &:focus { 154 | background-color: lighten(#2b90d9, 7%); 155 | transition: all 200ms ease-out; 156 | } 157 | 158 | &:disabled, &.disabled { 159 | background-color: #9baec8; 160 | cursor: default; 161 | } 162 | } 163 | 164 | .footer { 165 | text-align: center; 166 | color: lighten(#282c37, 26%); 167 | 168 | a { 169 | color: inherit; 170 | } 171 | } 172 | 173 | .info-area { 174 | margin-bottom: 20px; 175 | background: darken(#282c37, 8%); 176 | padding: 20px 10px; 177 | border-radius: 4px; 178 | color: #fff; 179 | font-size: 15px; 180 | 181 | li { 182 | margin-bottom: 5px; 183 | } 184 | 185 | strong { 186 | font-weight: 500; 187 | } 188 | } 189 | 190 | h4 { 191 | font-size: 18px; 192 | font-weight: 400; 193 | margin-bottom: 20px; 194 | color: #fff; 195 | } 196 | 197 | .user-map { 198 | margin-bottom: 10px; 199 | display: flex; 200 | line-height: 36px; 201 | 202 | & > div { 203 | flex: 0 0 auto; 204 | } 205 | 206 | .twitter { 207 | width: 150px; 208 | } 209 | 210 | .to { 211 | width: 30px; 212 | margin: 0 20px; 213 | } 214 | 215 | .mastodon { 216 | width: 200px; 217 | } 218 | 219 | .follow { 220 | flex: 1 1 auto; 221 | text-align: right; 222 | } 223 | } 224 | 225 | .header { 226 | margin-bottom: 30px; 227 | display: flex; 228 | justify-content: space-between; 229 | 230 | ul { 231 | list-style: none; 232 | 233 | li { 234 | display: inline-block; 235 | vertical-align: bottom; 236 | } 237 | 238 | a { 239 | display: flex; 240 | align-content: center; 241 | align-items: center; 242 | 243 | color: $lighter; 244 | text-decoration: none; 245 | padding: 12px 16px; 246 | line-height: 32px; 247 | transition: color $transition-out; 248 | font-family: 'Montserrat', sans-serif; 249 | font-weight: 500; 250 | font-size: 14px; 251 | 252 | img.link-logo { 253 | width: 32px; 254 | height: 32px; 255 | opacity: 0.5; 256 | transition: opacity $transition-out; 257 | 258 | &.before { 259 | margin-right: 10px; 260 | } 261 | 262 | &.after { 263 | margin-left: 10px; 264 | } 265 | } 266 | 267 | &.active, &:hover { 268 | color: $lightest; 269 | transition: color $transition-in; 270 | 271 | img.link-logo { 272 | opacity: 0.8; 273 | transition: opacity $transition-in; 274 | } 275 | } 276 | 277 | &.brand { 278 | color: $white; 279 | 280 | img { 281 | width: auto; 282 | height: 32px; 283 | opacity: 1; 284 | position: relative; 285 | top: 4px; 286 | } 287 | 288 | &:hover { 289 | border-bottom-color: transparent; 290 | 291 | img { 292 | opacity: 1; 293 | } 294 | } 295 | } 296 | } 297 | 298 | li:first-child a { 299 | padding-left: 0; 300 | } 301 | 302 | li:last-child a { 303 | padding-right: 0; 304 | } 305 | } 306 | } 307 | 308 | .page-heading { 309 | background: darken($darkest, 4%); 310 | padding: 20px; 311 | margin-bottom: 30px; 312 | border-radius: 10px; 313 | 314 | &.bottomless { 315 | border-radius: 10px 10px 0 0; 316 | } 317 | 318 | h3 { 319 | font-family: 'Montserrat', sans-serif; 320 | font-size: 26px; 321 | 322 | small { 323 | font-family: 'Roboto', sans-serif; 324 | margin-top: 20px; 325 | display: block; 326 | color: $lighter; 327 | font-size: 18px; 328 | } 329 | } 330 | } 331 | 332 | .connections { 333 | li { 334 | padding: 0 10px; 335 | display: flex; 336 | margin-bottom: 15px; 337 | } 338 | 339 | .connection-brand { 340 | width: 80px; 341 | height: 80px; 342 | 343 | img { 344 | display: block; 345 | margin: 0; 346 | } 347 | 348 | &.twitter { 349 | img { 350 | width: 80px; 351 | height: 80px; 352 | } 353 | } 354 | 355 | &.mastodon { 356 | background: #3088d4; 357 | 358 | img { 359 | width: 60px; 360 | height: 60px; 361 | margin: 10px; 362 | } 363 | } 364 | } 365 | 366 | .connection-info { 367 | margin-left: 15px; 368 | 369 | strong { 370 | font-weight: 500; 371 | } 372 | 373 | .connection-disconnect-link { 374 | display: block; 375 | } 376 | } 377 | } 378 | 379 | .login-buttons { 380 | display: flex; 381 | margin-bottom: 30px; 382 | 383 | & > a, & > div { 384 | flex: 1 0 0; 385 | color: #fff; 386 | display: flex; 387 | text-decoration: none; 388 | overflow: hidden; 389 | border-radius: 10px; 390 | 391 | &:first-child { 392 | margin-right: 10px; 393 | } 394 | } 395 | 396 | .login-brand { 397 | width: 80px; 398 | height: 80px; 399 | 400 | img { 401 | display: block; 402 | width: 100%; 403 | height: 100%; 404 | margin: 0; 405 | } 406 | } 407 | 408 | .login-content { 409 | padding: 18px 10px; 410 | } 411 | 412 | .login-numbers, .login-name { 413 | strong { 414 | font-weight: 500; 415 | } 416 | } 417 | 418 | .twitter { 419 | background: #1da1f2; 420 | } 421 | 422 | .mastodon { 423 | background: #3088d4; 424 | 425 | .login-brand img { 426 | width: 60px; 427 | height: 60px; 428 | margin: 10px; 429 | } 430 | } 431 | 432 | a.twitter:hover { 433 | background: lighten(#1da1f2, 4%); 434 | } 435 | 436 | a.mastodon:hover { 437 | background: lighten(#3088d4, 4%); 438 | } 439 | } 440 | 441 | .grid { 442 | width: 100%; 443 | display: flex; 444 | flex-wrap: wrap; 445 | margin-bottom: 20px; 446 | } 447 | 448 | .instance-card { 449 | display: block; 450 | text-decoration: none; 451 | color: inherit; 452 | border-radius: 10px; 453 | background-color: darken($darkest, 4%); 454 | background-size: cover; 455 | background-position: center; 456 | padding-top: 110px; 457 | margin-bottom: 20px; 458 | flex: 1 1 auto; 459 | max-width: 300px; 460 | width: calc(50% - 10px); 461 | margin-right: 10px; 462 | 463 | &:nth-child(even) { 464 | margin-right: 0; 465 | } 466 | 467 | .info { 468 | background: linear-gradient(0deg, rgba(#000, 0.8) 0, rgba(#000, 0.35) 60%, transparent); 469 | padding: 10px; 470 | padding-top: 40px; 471 | border-radius: 0 0 10px 10px; 472 | } 473 | 474 | .title { 475 | font-weight: 500; 476 | color: $lightest; 477 | display: block; 478 | } 479 | 480 | &:hover, 481 | &:focus, 482 | &:active { 483 | color: $lightest; 484 | 485 | .title { 486 | color: #fff; 487 | } 488 | } 489 | } 490 | 491 | .user-card { 492 | display: block; 493 | text-decoration: none; 494 | color: inherit; 495 | display: flex; 496 | align-items: center; 497 | flex: 1 1 auto; 498 | max-width: 300px; 499 | width: calc(50% - 10px); 500 | margin-bottom: 20px; 501 | margin-right: 10px; 502 | position: relative; 503 | box-sizing: border-box; 504 | padding: 10px; 505 | 506 | &:nth-child(even) { 507 | margin-right: 0; 508 | } 509 | 510 | .avatar { 511 | width: 60px; 512 | height: 60px; 513 | background: darken($darkest, 4%); 514 | border-radius: 50%; 515 | overflow: hidden; 516 | flex-shrink: 0; 517 | 518 | img { 519 | display: block; 520 | margin: 0; 521 | width: 100%; 522 | height: 100%; 523 | border-radius: 50%; 524 | } 525 | } 526 | 527 | .following-indicator { 528 | position: absolute; 529 | width: 25px; 530 | height: 25px; 531 | background: $vibrant; 532 | color: $white; 533 | text-align: center; 534 | border-radius: 50%; 535 | font-size: 16px; 536 | line-height: 25px; 537 | bottom: 5px; 538 | box-shadow: 3px -3px 6px rgba(#000, 0.3); 539 | } 540 | 541 | .name { 542 | margin-left: 10px; 543 | white-space: nowrap; 544 | overflow: hidden; 545 | text-overflow: ellipsis; 546 | } 547 | 548 | .display-name { 549 | font-weight: 500; 550 | color: $lightest; 551 | display: block; 552 | } 553 | 554 | &:hover, 555 | &:focus, 556 | &:active { 557 | color: $lightest; 558 | 559 | .display-name { 560 | color: #fff; 561 | } 562 | } 563 | } 564 | 565 | .empty-message { 566 | margin-bottom: 20px; 567 | } 568 | 569 | .pagination { 570 | text-align: center; 571 | margin-bottom: 20px; 572 | 573 | a { 574 | display: inline-block; 575 | text-transform: uppercase; 576 | font-weight: 500; 577 | text-decoration: none; 578 | } 579 | } 580 | 581 | .connect-prompt { 582 | background: $vibrant; 583 | margin-top: -30px; 584 | margin-bottom: 30px; 585 | color: $white; 586 | padding: 10px; 587 | border-radius: 0 0 5px 5px; 588 | text-align: center; 589 | 590 | a { 591 | color: $white; 592 | font-weight: 500; 593 | } 594 | } 595 | 596 | .hero-link { 597 | display: block; 598 | text-decoration: none; 599 | color: $white; 600 | 601 | img { 602 | width: 100%; 603 | height: auto; 604 | display: block; 605 | margin: 0; 606 | } 607 | 608 | margin-bottom: 30px; 609 | border-radius: 10px; 610 | overflow: hidden; 611 | } 612 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # Use this hook to configure devise mailer, warden hooks and so forth. 2 | # Many of these configuration options can be set straight in your model. 3 | Devise.setup do |config| 4 | # The secret key used by Devise. Devise uses this key to generate 5 | # random tokens. Changing this key will render invalid all existing 6 | # confirmation, reset password and unlock tokens in the database. 7 | # Devise will use the `secret_key_base` as its `secret_key` 8 | # by default. You can change it below and use your own secret key. 9 | # config.secret_key = 'c588a4de1cff751381b6bce2957ffc210910b6527cb4b39aa0b0d328316771487f0fbaccf6f3719be06cff95ec71247f4e0da242be3905401561f9cf465f28eb' 10 | 11 | # ==> Mailer Configuration 12 | # Configure the e-mail address which will be shown in Devise::Mailer, 13 | # note that it will be overwritten if you use your own mailer class 14 | # with default "from" parameter. 15 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 16 | 17 | # Configure the class responsible to send e-mails. 18 | # config.mailer = 'Devise::Mailer' 19 | 20 | # Configure the parent class responsible to send e-mails. 21 | # config.parent_mailer = 'ActionMailer::Base' 22 | 23 | # ==> ORM configuration 24 | # Load and configure the ORM. Supports :active_record (default) and 25 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 26 | # available as additional gems. 27 | require 'devise/orm/active_record' 28 | 29 | # ==> Configuration for any authentication mechanism 30 | # Configure which keys are used when authenticating a user. The default is 31 | # just :email. You can configure it to use [:username, :subdomain], so for 32 | # authenticating a user, both parameters are required. Remember that those 33 | # parameters are used only when authenticating and not when retrieving from 34 | # session. If you need permissions, you should implement that in a before filter. 35 | # You can also supply a hash where the value is a boolean determining whether 36 | # or not authentication should be aborted when the value is not present. 37 | # config.authentication_keys = [:email] 38 | 39 | # Configure parameters from the request object used for authentication. Each entry 40 | # given should be a request method and it will automatically be passed to the 41 | # find_for_authentication method and considered in your model lookup. For instance, 42 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 43 | # The same considerations mentioned for authentication_keys also apply to request_keys. 44 | # config.request_keys = [] 45 | 46 | # Configure which authentication keys should be case-insensitive. 47 | # These keys will be downcased upon creating or modifying a user and when used 48 | # to authenticate or find a user. Default is :email. 49 | config.case_insensitive_keys = [:email] 50 | 51 | # Configure which authentication keys should have whitespace stripped. 52 | # These keys will have whitespace before and after removed upon creating or 53 | # modifying a user and when used to authenticate or find a user. Default is :email. 54 | config.strip_whitespace_keys = [:email] 55 | 56 | # Tell if authentication through request.params is enabled. True by default. 57 | # It can be set to an array that will enable params authentication only for the 58 | # given strategies, for example, `config.params_authenticatable = [:database]` will 59 | # enable it only for database (email + password) authentication. 60 | # config.params_authenticatable = true 61 | 62 | # Tell if authentication through HTTP Auth is enabled. False by default. 63 | # It can be set to an array that will enable http authentication only for the 64 | # given strategies, for example, `config.http_authenticatable = [:database]` will 65 | # enable it only for database authentication. The supported strategies are: 66 | # :database = Support basic authentication with authentication key + password 67 | # config.http_authenticatable = false 68 | 69 | # If 401 status code should be returned for AJAX requests. True by default. 70 | # config.http_authenticatable_on_xhr = true 71 | 72 | # The realm used in Http Basic Authentication. 'Application' by default. 73 | # config.http_authentication_realm = 'Application' 74 | 75 | # It will change confirmation, password recovery and other workflows 76 | # to behave the same regardless if the e-mail provided was right or wrong. 77 | # Does not affect registerable. 78 | # config.paranoid = true 79 | 80 | # By default Devise will store the user in session. You can skip storage for 81 | # particular strategies by setting this option. 82 | # Notice that if you are skipping storage for all authentication paths, you 83 | # may want to disable generating routes to Devise's sessions controller by 84 | # passing skip: :sessions to `devise_for` in your config/routes.rb 85 | config.skip_session_storage = [:http_auth] 86 | 87 | # By default, Devise cleans up the CSRF token on authentication to 88 | # avoid CSRF token fixation attacks. This means that, when using AJAX 89 | # requests for sign in and sign up, you need to get a new CSRF token 90 | # from the server. You can disable this option at your own risk. 91 | # config.clean_up_csrf_token_on_authentication = true 92 | 93 | # When false, Devise will not attempt to reload routes on eager load. 94 | # This can reduce the time taken to boot the app but if your application 95 | # requires the Devise mappings to be loaded during boot time the application 96 | # won't boot properly. 97 | # config.reload_routes = true 98 | 99 | # ==> Configuration for :database_authenticatable 100 | # For bcrypt, this is the cost for hashing the password and defaults to 11. If 101 | # using other algorithms, it sets how many times you want the password to be hashed. 102 | # 103 | # Limiting the stretches to just one in testing will increase the performance of 104 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 105 | # a value less than 10 in other environments. Note that, for bcrypt (the default 106 | # algorithm), the cost increases exponentially with the number of stretches (e.g. 107 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 108 | config.stretches = Rails.env.test? ? 1 : 11 109 | 110 | # Set up a pepper to generate the hashed password. 111 | # config.pepper = '8004555ec2d9a63235797813526a495087593aaad309d47b6037443b2b0b645663720a394446a78300a2b8b55625f7d87edeaedb7baf8a298a2025eacdfd297e' 112 | 113 | # Send a notification email when the user's password is changed 114 | # config.send_password_change_notification = false 115 | 116 | # ==> Configuration for :confirmable 117 | # A period that the user is allowed to access the website even without 118 | # confirming their account. For instance, if set to 2.days, the user will be 119 | # able to access the website for two days without confirming their account, 120 | # access will be blocked just in the third day. Default is 0.days, meaning 121 | # the user cannot access the website without confirming their account. 122 | # config.allow_unconfirmed_access_for = 2.days 123 | 124 | # A period that the user is allowed to confirm their account before their 125 | # token becomes invalid. For example, if set to 3.days, the user can confirm 126 | # their account within 3 days after the mail was sent, but on the fourth day 127 | # their account can't be confirmed with the token any more. 128 | # Default is nil, meaning there is no restriction on how long a user can take 129 | # before confirming their account. 130 | # config.confirm_within = 3.days 131 | 132 | # If true, requires any email changes to be confirmed (exactly the same way as 133 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 134 | # db field (see migrations). Until confirmed, new email is stored in 135 | # unconfirmed_email column, and copied to email column on successful confirmation. 136 | config.reconfirmable = true 137 | 138 | # Defines which key will be used when confirming an account 139 | # config.confirmation_keys = [:email] 140 | 141 | # ==> Configuration for :rememberable 142 | # The time the user will be remembered without asking for credentials again. 143 | # config.remember_for = 2.weeks 144 | 145 | # Invalidates all the remember me tokens when the user signs out. 146 | config.expire_all_remember_me_on_sign_out = true 147 | 148 | # If true, extends the user's remember period when remembered via cookie. 149 | # config.extend_remember_period = false 150 | 151 | # Options to be passed to the created cookie. For instance, you can set 152 | # secure: true in order to force SSL only cookies. 153 | # config.rememberable_options = {} 154 | 155 | # ==> Configuration for :validatable 156 | # Range for password length. 157 | config.password_length = 6..128 158 | 159 | # Email regex used to validate email formats. It simply asserts that 160 | # one (and only one) @ exists in the given string. This is mainly 161 | # to give user feedback and not to assert the e-mail validity. 162 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ 163 | 164 | # ==> Configuration for :timeoutable 165 | # The time you want to timeout the user session without activity. After this 166 | # time the user will be asked for credentials again. Default is 30 minutes. 167 | # config.timeout_in = 30.minutes 168 | 169 | # ==> Configuration for :lockable 170 | # Defines which strategy will be used to lock an account. 171 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 172 | # :none = No lock strategy. You should handle locking by yourself. 173 | # config.lock_strategy = :failed_attempts 174 | 175 | # Defines which key will be used when locking and unlocking an account 176 | # config.unlock_keys = [:email] 177 | 178 | # Defines which strategy will be used to unlock an account. 179 | # :email = Sends an unlock link to the user email 180 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 181 | # :both = Enables both strategies 182 | # :none = No unlock strategy. You should handle unlocking by yourself. 183 | # config.unlock_strategy = :both 184 | 185 | # Number of authentication tries before locking an account if lock_strategy 186 | # is failed attempts. 187 | # config.maximum_attempts = 20 188 | 189 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 190 | # config.unlock_in = 1.hour 191 | 192 | # Warn on the last attempt before the account is locked. 193 | # config.last_attempt_warning = true 194 | 195 | # ==> Configuration for :recoverable 196 | # 197 | # Defines which key will be used when recovering the password for an account 198 | # config.reset_password_keys = [:email] 199 | 200 | # Time interval you can reset your password with a reset password key. 201 | # Don't put a too small interval or your users won't have the time to 202 | # change their passwords. 203 | config.reset_password_within = 6.hours 204 | 205 | # When set to false, does not sign a user in automatically after their password is 206 | # reset. Defaults to true, so a user is signed in automatically after a reset. 207 | # config.sign_in_after_reset_password = true 208 | 209 | # ==> Configuration for :encryptable 210 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default). 211 | # You can use :sha1, :sha512 or algorithms from others authentication tools as 212 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 213 | # for default behavior) and :restful_authentication_sha1 (then you should set 214 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). 215 | # 216 | # Require the `devise-encryptable` gem when using anything other than bcrypt 217 | # config.encryptor = :sha512 218 | 219 | # ==> Scopes configuration 220 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 221 | # "users/sessions/new". It's turned off by default because it's slower if you 222 | # are using only default views. 223 | # config.scoped_views = false 224 | 225 | # Configure the default scope given to Warden. By default it's the first 226 | # devise role declared in your routes (usually :user). 227 | # config.default_scope = :user 228 | 229 | # Set this configuration to false if you want /users/sign_out to sign out 230 | # only the current scope. By default, Devise signs out all scopes. 231 | # config.sign_out_all_scopes = true 232 | 233 | # ==> Navigation configuration 234 | # Lists the formats that should be treated as navigational. Formats like 235 | # :html, should redirect to the sign in page when the user does not have 236 | # access, but formats like :xml or :json, should return 401. 237 | # 238 | # If you have any extra navigational formats, like :iphone or :mobile, you 239 | # should add them to the navigational formats lists. 240 | # 241 | # The "*/*" below is required to match Internet Explorer requests. 242 | # config.navigational_formats = ['*/*', :html] 243 | 244 | # The default HTTP method used to sign out a resource. Default is :delete. 245 | config.sign_out_via = :delete 246 | 247 | # ==> OmniAuth 248 | # Add a new OmniAuth provider. Check the wiki for more information on setting 249 | # up on your models and hooks. 250 | config.omniauth :twitter, ENV['TWITTER_CLIENT_ID'], ENV['TWITTER_CLIENT_SECRET'] 251 | 252 | config.omniauth :mastodon, scope: 'read follow', credentials: lambda { |domain, callback_url| 253 | client = MastodonClient.where(domain: domain).first 254 | 255 | if client.nil? 256 | client = MastodonClient.obtain!(domain, callback_url) 257 | else 258 | still_valid = Rails.cache.fetch("client-status/#{client.id}") { client.still_valid? } 259 | 260 | unless still_valid 261 | client.destroy! 262 | client = MastodonClient.obtain!(domain, callback_url) 263 | end 264 | end 265 | 266 | [client.client_id, client.client_secret] 267 | } 268 | 269 | # ==> Warden configuration 270 | # If you want to use other strategies, that are not supported by Devise, or 271 | # change the failure app, you can configure them inside the config.warden block. 272 | # 273 | # config.warden do |manager| 274 | # manager.intercept_401 = false 275 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 276 | # end 277 | 278 | # ==> Mountable engine configurations 279 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 280 | # is mountable, there are some extra configurations to be taken into account. 281 | # The following options are available, assuming the engine is mounted as: 282 | # 283 | # mount MyEngine, at: '/my_engine' 284 | # 285 | # The router that invoked `devise_for`, in the example above, would be: 286 | # config.router_name = :my_engine 287 | # 288 | # When using OmniAuth, Devise cannot automatically set OmniAuth path, 289 | # so you need to do it manually. For the users scope, it would be: 290 | # config.omniauth_path_prefix = '/my_engine/users/auth' 291 | end 292 | --------------------------------------------------------------------------------