+1
.gitignore
+1
.gitignore
···
···
1
+
config/*.yml
+9
Gemfile
+9
Gemfile
+57
Gemfile.lock
+57
Gemfile.lock
···
···
1
+
GIT
2
+
remote: https://github.com/mastodon/mastodon-api.git
3
+
revision: 60b0ed09c3b979cbeb833db0a320b30e1b022a1e
4
+
specs:
5
+
mastodon-api (2.0.0)
6
+
addressable (~> 2.6)
7
+
buftok (~> 0)
8
+
http (~> 4.0)
9
+
oj (~> 3.7)
10
+
11
+
GEM
12
+
remote: https://rubygems.org/
13
+
specs:
14
+
addressable (2.8.6)
15
+
public_suffix (>= 2.0.2, < 6.0)
16
+
bigdecimal (3.1.7)
17
+
buftok (0.3.0)
18
+
domain_name (0.6.20240107)
19
+
ffi (1.16.3)
20
+
ffi-compiler (1.3.2)
21
+
ffi (>= 1.15.5)
22
+
rake
23
+
http (4.4.1)
24
+
addressable (~> 2.3)
25
+
http-cookie (~> 1.0)
26
+
http-form_data (~> 2.2)
27
+
http-parser (~> 1.2.0)
28
+
http-cookie (1.0.5)
29
+
domain_name (~> 0.5)
30
+
http-form_data (2.3.0)
31
+
http-parser (1.2.3)
32
+
ffi-compiler (>= 1.0, < 2.0)
33
+
io-console (0.7.2)
34
+
json (2.7.1)
35
+
net-http (0.4.1)
36
+
uri
37
+
oj (3.16.3)
38
+
bigdecimal (>= 3.0)
39
+
public_suffix (5.0.4)
40
+
rake (13.1.0)
41
+
uri (0.13.0)
42
+
yaml (0.3.0)
43
+
44
+
PLATFORMS
45
+
arm64-darwin-21
46
+
ruby
47
+
48
+
DEPENDENCIES
49
+
io-console (~> 0.5)
50
+
json (~> 2.5)
51
+
mastodon-api!
52
+
net-http (~> 0.2)
53
+
uri (~> 0.13)
54
+
yaml (~> 0.1)
55
+
56
+
BUNDLED WITH
57
+
2.5.6
+44
app/mastodon_account.rb
+44
app/mastodon_account.rb
···
···
1
+
require 'mastodon'
2
+
require 'yaml'
3
+
4
+
require_relative 'mastodon_api'
5
+
6
+
class MastodonAccount
7
+
APP_NAME = "tootify"
8
+
CONFIG_FILE = File.expand_path(File.join(__dir__, '..', 'config', 'mastodon.yml'))
9
+
OAUTH_SCOPES = 'read:accounts read:statuses write:media write:statuses'
10
+
11
+
def initialize
12
+
@config = File.exist?(CONFIG_FILE) ? YAML.load(File.read(CONFIG_FILE)) : {}
13
+
end
14
+
15
+
def save_config
16
+
File.write(CONFIG_FILE, YAML.dump(@config))
17
+
end
18
+
19
+
def oauth_login(handle, email, password)
20
+
instance = handle.split('@').last
21
+
app_response = register_oauth_app(instance, OAUTH_SCOPES)
22
+
23
+
api = MastodonAPI.new(instance)
24
+
25
+
json = api.oauth_login_with_password(
26
+
app_response.client_id,
27
+
app_response.client_secret,
28
+
email, password, OAUTH_SCOPES
29
+
)
30
+
31
+
api.access_token = json['access_token']
32
+
info = api.account_info
33
+
34
+
@config['handle'] = handle
35
+
@config['access_token'] = api.access_token
36
+
@config['user_id'] = info['id']
37
+
save_config
38
+
end
39
+
40
+
def register_oauth_app(instance, scopes)
41
+
client = Mastodon::REST::Client.new(base_url: "https://#{instance}")
42
+
client.create_app(APP_NAME, 'urn:ietf:wg:oauth:2.0:oob', scopes)
43
+
end
44
+
end
+88
app/mastodon_api.rb
+88
app/mastodon_api.rb
···
···
1
+
require 'json'
2
+
require 'net/http'
3
+
require 'uri'
4
+
5
+
class MastodonAPI
6
+
class UnauthenticatedError < StandardError
7
+
end
8
+
9
+
class UnexpectedResponseError < StandardError
10
+
end
11
+
12
+
class APIError < StandardError
13
+
attr_reader :response
14
+
15
+
def initialize(response)
16
+
@response = response
17
+
super("APIError #{response.code}: #{response.body}")
18
+
end
19
+
20
+
def status
21
+
response.code.to_i
22
+
end
23
+
end
24
+
25
+
attr_accessor :access_token
26
+
27
+
def initialize(host, access_token = nil)
28
+
@host = host
29
+
@root = "https://#{@host}/api/v1"
30
+
@access_token = access_token
31
+
end
32
+
33
+
def oauth_login_with_password(client_id, client_secret, email, password, scopes)
34
+
url = URI("https://#{@host}/oauth/token")
35
+
36
+
params = {
37
+
client_id: client_id,
38
+
client_secret: client_secret,
39
+
grant_type: 'password',
40
+
scope: scopes,
41
+
username: email,
42
+
password: password
43
+
}
44
+
45
+
response = Net::HTTP.post_form(url, params)
46
+
status = response.code.to_i
47
+
48
+
if status / 100 == 2
49
+
JSON.parse(response.body)
50
+
else
51
+
raise APIError.new(response)
52
+
end
53
+
end
54
+
55
+
def account_info
56
+
raise UnauthenticatedError.new unless @access_token
57
+
get_json("/accounts/verify_credentials")
58
+
end
59
+
60
+
def lookup_account(username)
61
+
json = get_json("/accounts/lookup", { acct: username })
62
+
raise UnexpectedResponseError.new unless json.is_a?(Hash) && json['id'].is_a?(String)
63
+
json
64
+
end
65
+
66
+
def account_statuses(user_id, params = {})
67
+
get_json("/accounts/#{user_id}/statuses", params)
68
+
end
69
+
70
+
def get_json(path, params = {})
71
+
url = URI(path.start_with?('https://') ? path : @root + path)
72
+
url.query = URI.encode_www_form(params) if params
73
+
74
+
headers = {}
75
+
headers['Authorization'] = "Bearer #{@access_token}" if @access_token
76
+
77
+
response = Net::HTTP.get_response(url, headers)
78
+
status = response.code.to_i
79
+
80
+
if status / 100 == 2
81
+
JSON.parse(response.body)
82
+
elsif status / 100 == 3
83
+
get_json(response['Location'])
84
+
else
85
+
raise APIError.new(response)
86
+
end
87
+
end
88
+
end
+22
app/tootify.rb
+22
app/tootify.rb
···
···
1
+
require 'io/console'
2
+
require_relative 'mastodon_account'
3
+
4
+
class Tootify
5
+
def initialize
6
+
@mastodon = MastodonAccount.new
7
+
end
8
+
9
+
def login_bluesky
10
+
end
11
+
12
+
def login_mastodon(handle)
13
+
print "Email: "
14
+
email = STDIN.gets.chomp
15
+
16
+
print "Password: "
17
+
password = STDIN.noecho(&:gets).chomp
18
+
puts
19
+
20
+
@mastodon.oauth_login(handle, email, password)
21
+
end
22
+
end
+39
tootify
+39
tootify
···
···
1
+
#!/usr/bin/env ruby
2
+
3
+
require 'bundler/setup'
4
+
require_relative 'app/tootify'
5
+
6
+
$app = Tootify.new
7
+
8
+
def run(argv)
9
+
options, args = argv.partition { |x| x.start_with?('-') }
10
+
11
+
case args.first
12
+
when 'login'
13
+
login(args[1])
14
+
when 'check'
15
+
else
16
+
print_help
17
+
end
18
+
end
19
+
20
+
def print_help
21
+
puts "Usage: #{$PROGRAM_NAME} login mastodon@account | login @bluesky"
22
+
puts " #{$PROGRAM_NAME} check"
23
+
exit 1
24
+
end
25
+
26
+
def login(name)
27
+
if name =~ /\A[^@]+@[^@]+\z/
28
+
$app.login_mastodon(name)
29
+
elsif name =~ /\A@[^@]+\z/
30
+
$app.login_bluesky(name)
31
+
elsif name.nil?
32
+
print_help
33
+
else
34
+
puts "Invalid handle: #{name.inspect}"
35
+
exit 1
36
+
end
37
+
end
38
+
39
+
run(ARGV)