Skip to content

Commit

Permalink
Add Mastodon connector to settings / profile (#1173)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcharter committed Feb 2, 2024
1 parent 0037cf4 commit 67c1959
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 4 deletions.
40 changes: 37 additions & 3 deletions app/controllers/settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,40 @@ def pushover_callback
redirect_to "/settings"
end

def mastodon_auth
app = MastodonApp.find_or_register(params[:mastodon_instance_name])
redirect_to app.oauth_auth_url, allow_other_host: true
end

def mastodon_callback
if params[:code].blank?
flash[:error] = "Invalid OAuth state"
return redirect_to settings_path
end

app = MastodonApp.find_or_register(params[:instance])
tok, username = app.token_and_user_from_code(params[:code])
if tok.present? && username.present?
@user.mastodon_oauth_token = tok
@user.mastodon_username = username
@user.mastodon_instance = params[:instance]
@user.save!
flash[:success] = "Linked to Mastodon user @#{username}@#{app.name}."
else
return mastodon_disconnect
end

redirect_to settings_path
end

def mastodon_disconnect
@user.mastodon_oauth_token = nil
@user.mastodon_username = nil
@user.save!
flash[:success] = "Your Mastodon association has been removed."
redirect_to settings_path
end

def github_auth
session[:github_state] = SecureRandom.hex
redirect_to Github.oauth_auth_url(session[:github_state]), allow_other_host: true
Expand All @@ -196,7 +230,7 @@ def github_callback
params[:code].blank? ||
(params[:state].to_s != session[:github_state].to_s)
flash[:error] = "Invalid OAuth state"
return redirect_to "/settings"
return redirect_to settings_path
end

session.delete(:github_state)
Expand All @@ -211,15 +245,15 @@ def github_callback
return github_disconnect
end

redirect_to "/settings"
redirect_to settings_path
end

def github_disconnect
@user.github_oauth_token = nil
@user.github_username = nil
@user.save!
flash[:success] = "Your GitHub association has been removed."
redirect_to "/settings"
redirect_to settings_path
end

def twitter_auth
Expand Down
91 changes: 91 additions & 0 deletions app/models/mastodon_app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# typed: false

# https://docs.joinmastodon.org/methods/apps/
class MastodonApp < ApplicationRecord
validates :name, :client_id, :client_secret, presence: true

# https://docs.joinmastodon.org/methods/oauth/
def oauth_auth_url
"https://#{name}/oauth/authorize?response_type=code&client_id=#{client_id}&scope=read:accounts&redirect_uri=" +
CGI.escape(redirect_uri)
end

def redirect_uri
"https://#{Rails.application.domain}/settings/mastodon_callback?instance=#{name}"
end

def register_app
raise "already registered, delete and recreate" if client_id.present?

s = Sponge.new
url = "https://#{name}/api/v1/apps"
res = s.fetch(
url,
:post,
client_name: Rails.application.domain,
redirect_uris: [
"https://#{Rails.application.domain}/settings",
redirect_uri
].join("\n"),
scopes: "read:accounts",
website: "https://#{Rails.application.domain}"
)
js = JSON.parse(res.body)
if js && js["client_id"].present? && js["client_secret"].present?
self.client_id = js["client_id"]
self.client_secret = js["client_secret"]
return save!
end
raise "registration failed, response was #{res.body}"
end

def token_and_user_from_code(code)
s = Sponge.new
res = s.fetch(
"https://#{name}/oauth/token",
:post,
client_id: client_id,
client_secret: client_secret,
redirect_uri: redirect_uri,
grant_type: "authorization_code",
code: code,
scope: "read:account"
)
ps = JSON.parse(res.body)
tok = ps["access_token"]

if tok.present?
headers = {"Authorization" => "Bearer #{tok}"}
res = s.fetch(
"https://#{name}/api/v1/accounts/verify_credentials",
:get,
nil,
nil,
headers
).body
js = JSON.parse(res)
if js && js["username"].present?
return [tok, js["username"]]
end
end

[nil, nil]
end

def self.find_or_register(instance_name)
name = sanitized_instance_name(instance_name)
existing = find_by name: name
return existing if existing.present?

app = new name: name
app.register_app
app.save!
app
end

# extract hostname from possible URL
def self.sanitized_instance_name(instance_name)
instance_name.delete_prefix "https://"
instance_name.split("/").first
end
end
3 changes: 3 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class User < ApplicationRecord
s.string :totp_secret
s.string :github_oauth_token
s.string :github_username
s.string :mastodon_instance
s.string :mastodon_oauth_token
s.string :mastodon_username
s.string :twitter_oauth_token
s.string :twitter_oauth_token_secret
s.string :twitter_username
Expand Down
18 changes: 18 additions & 0 deletions app/views/settings/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,24 @@
</div>
<% end %>
<% if Mastodon.enabled? %>
<div class="boxline">
<span>
<%= label_tag :mastodon_username,
raw("<a href=\"https://joinmastodon.org/\">Mastodon</a>:"),
:class => "required" %>
<% if @edit_user.mastodon_username.present? %>
Linked to
<strong><a href="https://<%= @edit_user.mastodon_instance %>/@<%= h(@edit_user.mastodon_username)
%>">@<%= h(@edit_user.mastodon_username) %>@<%=@edit_user.mastodon_instance%></a></strong>
<%= link_post "Disconnect", "/settings/mastodon_disconnect" %>
<% else %>
<a href="/settings/mastodon_authentication">Connect</a>
<% end %>
</span>
</div>
<% end %>
<% if Twitter.enabled? %>
<div class="boxline">
<%= label_tag :twitter_username,
Expand Down
20 changes: 20 additions & 0 deletions app/views/settings/mastodon_authentication.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<% content_for :subnav do %>
<a href="/settings">Back to Settings</a>
<% end %>

<div class="box wide">
<%= form_with url: "/settings/mastodon_auth", method: :get do |f| %>
<p>
Enter the name or URL of your Mastodon instance (for example, mastodon.social):
</p>

<div class="boxline">
<%= f.label :mastodon_instance_name, "Name or URL:", :class => "required" %>
<%= f.text_field :mastodon_instance_name, :autofocus => true %>
</div>

<p>
<%= f.submit "Continue" %>
<% end %>
</div>
11 changes: 11 additions & 0 deletions app/views/users/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@
<br>
<% end %>
<% if @showing_user.mastodon_username.present? && @showing_user.mastodon_instance.present? %>
<label class="required">Mastodon:</label>

<span class="d">
<a href="https://<%= h(@showing_user.mastodon_instance)%>/@<%= h(@showing_user.mastodon_username)%>"
rel="me ugc">@<%= h(@showing_user.mastodon_username)%>@<%= h(@showing_user.mastodon_instance)%>
</a>
</span>
<br>
<% end %>
<% if @showing_user.twitter_username.present? %>
<label class="required">Twitter:</label>

Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,13 @@
:as => "twofa_verify"
post "/settings/2fa_update" => "settings#twofa_update",
:as => "twofa_update"
get "/settings/mastodon_authentication" => "settings#mastodon_authentication"

post "/settings/pushover_auth" => "settings#pushover_auth"
get "/settings/pushover_callback" => "settings#pushover_callback"
get "/settings/mastodon_auth" => "settings#mastodon_auth"
get "/settings/mastodon_callback" => "settings#mastodon_callback"
post "/settings/mastodon_disconnect" => "settings#mastodon_disconnect"
get "/settings/github_auth" => "settings#github_auth"
get "/settings/github_callback" => "settings#github_callback"
post "/settings/github_disconnect" => "settings#github_disconnect"
Expand Down
13 changes: 13 additions & 0 deletions db/migrate/20230424222719_create_mastodon_apps.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CreateMastodonApps < ActiveRecord::Migration[7.1]
def change
create_table :mastodon_apps do |t|
t.string :name, null: false
t.string :client_id, null: false
t.string :client_secret, null: false

t.timestamps
end

add_index :mastodon_apps, :name, unique: true
end
end
11 changes: 10 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_10_23_155620) do
ActiveRecord::Schema[7.1].define(version: 2023_10_23_155620) do
create_table "categories", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "category"
t.datetime "created_at", precision: nil, null: false
Expand Down Expand Up @@ -116,6 +116,15 @@
t.index ["key"], name: "key", unique: true
end

create_table "mastodon_apps", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.string "name", null: false
t.string "client_id", null: false
t.string "client_secret", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_mastodon_apps_on_name", unique: true
end

create_table "messages", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.datetime "created_at", precision: nil
t.bigint "author_user_id", unsigned: true
Expand Down
7 changes: 7 additions & 0 deletions extras/mastodon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# typed: false

class Mastodon
def self.enabled?
Rails.env.production?
end
end
4 changes: 4 additions & 0 deletions spec/models/mastodon_app_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# typed: false

RSpec.describe MastodonApp, type: :model do
end

0 comments on commit 67c1959

Please sign in to comment.