A curated list of libraries & SDKs for the Bluesky API and AT Protocol
1require_relative 'cargo_import'
2require_relative 'import_helpers'
3require_relative 'npm_import'
4require_relative 'requests'
5
6require 'didkit'
7require 'fileutils'
8require 'json'
9require 'licensee'
10require 'minisky'
11require 'time'
12
13class TangledImport
14 include ImportHelpers
15 include Requests
16
17 def initialize
18 @user_cache = {}
19 end
20
21 def url_matches?(url)
22 url =~ %r{^https://tangled\.org/@?[\w\-\.\:]+/[\w\-\.]+}
23 end
24
25 def import_url(url, project)
26 url =~ %r{^https://tangled\.org/@?([\w\-\.\:]+)/([\w\-\.]+)}
27 user, repo = $1, $2
28
29 did = DID.resolve_handle(user)
30 sky = Minisky.new(did.document.pds_host, nil)
31
32 repos = sky.fetch_all('com.atproto.repo.listRecords',
33 { repo: did, collection: 'sh.tangled.repo', limit: 100 },
34 field: 'records'
35 )
36
37 repo_record = repos.detect { |x| x['value']['name'] == repo }
38
39 repo_folder = clone_repo(user, repo)
40
41 data = repo_data_from_record(repo_record['value'])
42 data['user_login'] = user
43 data['user_profile'] = "https://tangled.org/#{user}"
44
45 if tag_info = get_latest_tag(repo_folder)
46 data['last_tag'] = tag_info
47 end
48
49 if (license = Licensee.license(repo_folder)) && license.spdx_id != 'NOASSERTION'
50 data['license'] = license.spdx_id
51 end
52
53 data['stars'] = get_stars(repo_record['uri'])
54
55 if project.language == :js
56 npm = get_npm_releases(repo_folder, project)
57 last_update = npm.map { |n| n.last_release_time }.sort.last
58
59 if last_update
60 data['last_release'] = { 'published_at' => last_update }
61 end
62 elsif project.language == :rust
63 crates = get_cargo_releases(repo_folder, project)
64 last_update = crates.map { |c| c.last_release_time }.sort.last
65
66 if last_update
67 data['last_release'] = { 'published_at' => last_update }
68 end
69 end
70
71 data['last_commit'] = get_latest_commit(repo_folder)
72
73 data
74 end
75
76 def clone_repo(user, repo)
77 repos_cache = File.expand_path(File.join(__dir__, '..', 'tmp', 'repos'))
78 FileUtils.mkdir_p(repos_cache)
79
80 dirname = "#{user}_#{repo}"
81 repo_folder = File.join(repos_cache, dirname)
82
83 if Dir.exist?(repo_folder)
84 Dir.chdir(repo_folder) do
85 system('git pull -q')
86 end
87 else
88 Dir.chdir(repos_cache) do
89 system("git clone https://tangled.org/#{user}/#{repo} #{dirname}")
90 end
91 end
92
93 repo_folder
94 end
95
96 def repo_data_from_record(record)
97 {
98 'name' => record['name'],
99 'description' => record['description'],
100 }
101 end
102
103 def get_latest_tag(repo_folder)
104 Dir.chdir(repo_folder) do
105 newest_tag_commit = %x(git rev-list --tags --max-count=1).strip
106 return nil if newest_tag_commit.empty?
107
108 newest_tag = %x(git tag --points-at "#{newest_tag_commit}" | head -1).strip
109 timestamp = %x(git show -s --format=%cI "#{newest_tag_commit}").strip
110
111 {
112 'name' => newest_tag,
113 'committer_date' => Time.parse(timestamp)
114 }
115 end
116 end
117
118 def get_latest_commit(repo_folder)
119 Dir.chdir(repo_folder) do
120 {
121 'committer_date' => %x(git show -s --format=%cI).strip.then { |x| Time.parse(x) },
122 'author_date' => %x(git show -s --format=%aI).strip.then { |x| Time.parse(x) }
123 }
124 end
125 end
126
127 def get_stars(repo_record_uri)
128 url = URI("https://constellation.microcosm.blue/links/count")
129 url.query = URI.encode_www_form(target: repo_record_uri, collection: 'sh.tangled.feed.star', path: '.subject')
130
131 response = get_response(url)
132 raise FetchError.new(response) unless response.code.to_i == 200
133
134 json = JSON.parse(response.body)
135 json['total']
136 end
137
138 def get_npm_releases(repo_folder, project)
139 Dir.chdir(repo_folder) do
140 packages = %x(find . -name 'package.json').strip
141 releases = []
142
143 packages.lines.each do |file|
144 contents = File.read(file.strip)
145 package = NPMImport::PackageJSON.new(contents)
146
147 next if package.version.nil? || package.private?
148
149 if npm = NPMImport.new.get_package_info(package.name)
150 package_repo_url = normalize_repo_url(npm.repository_url)
151 package_homepage = normalize_repo_url(npm.homepage_url)
152
153 if project.urls.map { |u| normalize_repo_url(u) }.any? { |u| u == package_repo_url || u == package_homepage }
154 releases << npm
155 end
156 end
157 end
158
159 releases
160 end
161 end
162
163 def get_cargo_releases(repo_folder, project)
164 Dir.chdir(repo_folder) do
165 packages = %x(find . -name 'Cargo.toml').strip
166 releases = []
167
168 packages.lines.each do |file|
169 contents = File.read(file.strip)
170 package = CargoImport::CargoToml.new(contents)
171
172 next if package.name.nil?
173
174 if crate = CargoImport.new.get_crate_info(package.name)
175 crate_repo_url = normalize_repo_url(crate.repository_url)
176 crate_homepage = normalize_repo_url(crate.homepage_url)
177
178 if project.urls.map { |u| normalize_repo_url(u) }.any? { |u| u == crate_repo_url || u == crate_homepage }
179 releases << crate
180 end
181 end
182 end
183
184 releases
185 end
186 end
187end