domain.ex (7047B)
1 # Zenflows is designed to implement the Valueflows vocabulary, 2 # written and maintained by srfsh <info@dyne.org>. 3 # Copyright (C) 2021-2023 Dyne.org foundation <foundation@dyne.org>. 4 # 5 # This program is free software: you can redistribute it and/or modify 6 # it under the terms of the GNU Affero General Public License as published by 7 # the Free Software Foundation, either version 3 of the License, or 8 # (at your option) any later version. 9 # 10 # This program is distributed in the hope that it will be useful, 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 # GNU Affero General Public License for more details. 14 # 15 # You should have received a copy of the GNU Affero General Public License 16 # along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 defmodule Zenflows.SWPass.Domain do 19 @moduledoc """ 20 Domain logic of interacting with softwarepassport instances over 21 HTTP. 22 """ 23 24 alias Zenflows.DB.Repo 25 alias Zenflows.VF.{ 26 Agent, 27 EconomicEvent, 28 EconomicResource, 29 Person, 30 Process, 31 ResourceSpecification, 32 Unit, 33 } 34 alias Ecto.Multi 35 36 # TODO: not the best piece of code, but will do okay for now. 37 @doc """ 38 Import repos by a URL to `/repositories` route of a softwarepassport 39 instance, and generate necessary things to link Person Agents to 40 EconomicResource Procjets. 41 """ 42 @spec import_repos(String.t()) :: {:ok, term()} | {:error, term()} 43 def import_repos(url) do 44 url = to_charlist(url) 45 hdrs = [ 46 {'user-agent', useragent()}, 47 {'accept', 'application/json'}, 48 ] 49 http_opts = [ 50 {:timeout, 30_000}, # 30 seconds 51 {:connect_timeout, 5000}, # 5 seconds 52 {:autoredirect, false}, 53 ] 54 with {:ok, {{_, 200, _}, _, body_charlist}} <- 55 :httpc.request(:get, {url, hdrs}, http_opts, []), 56 {:ok, map} <- body_charlist |> to_string() |> Jason.decode() do 57 proj = get_emails(map) 58 59 result = Enum.map(proj, fn {url, emails} -> 60 now = DateTime.utc_now() 61 mult = 62 Multi.new() 63 |> Multi.run(:commit_spec, fn repo, _ -> 64 params = %{name: "committing"} 65 66 case repo.get_by(ResourceSpecification, params) do 67 nil -> 68 params 69 |> ResourceSpecification.changeset() 70 |> repo.insert() 71 res_spec -> {:ok, res_spec} 72 end 73 end) 74 |> Multi.run(:proj_spec, fn repo, _ -> 75 params = %{name: "project"} 76 77 case repo.get_by(ResourceSpecification, params) do 78 nil -> 79 params 80 |> ResourceSpecification.changeset() 81 |> repo.insert() 82 res_spec -> {:ok, res_spec} 83 end 84 end) 85 |> Multi.run(:unit, fn repo, _ -> 86 params = %{label: "one", symbol: "one"} 87 88 case repo.get_by(Unit, params) do 89 nil -> 90 params 91 |> Unit.changeset() 92 |> repo.insert() 93 unit -> {:ok, unit} 94 end 95 end) 96 |> Multi.run(:proc, fn repo, _ -> 97 params = %{name: url} 98 99 case repo.get_by(Process, params) do 100 nil -> 101 params 102 |> Process.changeset() 103 |> repo.insert() 104 proc -> {:ok, proc} 105 end 106 end) 107 Enum.reduce(emails, mult, fn e, mult -> 108 mult 109 |> Multi.run("per:#{e}", fn repo, _ -> 110 params = %{email: e, user: e, name: e} 111 112 case repo.get_by(Person, params) do 113 nil -> 114 params 115 |> Person.changeset() 116 |> repo.insert() 117 per -> {:ok, per} 118 end 119 end) 120 |> Multi.run("evt:#{e}", fn repo, %{proc: proc, commit_spec: commit_spec} = chgs -> 121 per = Map.fetch!(chgs, "per:#{e}") 122 params = %{ 123 action_id: "deliverService", 124 input_of_id: proc.id, 125 provider_id: per.id, 126 receiver_id: per.id, 127 resource_conforms_to_id: commit_spec.id, 128 note: url, 129 } 130 131 case repo.get_by(EconomicEvent, params) do 132 nil -> 133 params 134 |> Map.put(:has_point_in_time, now) 135 |> EconomicEvent.changeset() 136 |> repo.insert() 137 evt -> {:ok, evt} 138 end 139 end) 140 end) 141 |> Multi.merge(fn changes -> 142 first_email = emails |> MapSet.to_list() |> List.first() 143 first_person = Map.fetch!(changes, "per:#{first_email}") 144 Multi.put(Multi.new(), :org, first_person) # TODO: make this really an organization 145 end) 146 |> Multi.run(:produce, fn repo, %{org: org, proc: proc, proj_spec: proj_spec, unit: unit} -> 147 params = %{ 148 action_id: "produce", 149 output_of_id: proc.id, 150 provider_id: org.id, 151 receiver_id: org.id, 152 resource_conforms_to_id: proj_spec.id, 153 note: url, 154 } 155 156 case repo.get_by(EconomicEvent, 157 params 158 |> Map.put(:resource_quantity_has_unit_id, unit.id) 159 |> Map.put(:resource_quantity_has_numerical_value, 1) 160 ) do 161 nil -> 162 params 163 |> Map.put(:resource_quantity, %{ 164 has_unit_id: unit.id, 165 has_numerical_value: 1, 166 }) 167 |> Map.put(:has_point_in_time, now) 168 |> EconomicEvent.changeset() 169 |> repo.insert() 170 evt -> {:ok, evt} 171 end 172 end) 173 |> Multi.run(:resource, fn repo, %{unit: unit, org: org, proj_spec: proj_spec} -> 174 params = %{ 175 name: url, 176 primary_accountable_id: org.id, 177 custodian_id: org.id, 178 conforms_to_id: proj_spec.id, 179 accounting_quantity_has_unit_id: unit.id, 180 accounting_quantity_has_numerical_value: 1, 181 onhand_quantity_has_unit_id: unit.id, 182 onhand_quantity_has_numerical_value: 1, 183 } 184 185 case repo.get_by(EconomicResource, params) do 186 nil -> 187 params 188 |> EconomicResource.changeset() 189 |> repo.insert() 190 res -> {:ok, res} 191 end 192 end) 193 |> Multi.run(:update_produce_evt, fn repo, %{produce: evt, resource: res} -> 194 Ecto.Changeset.change(evt, resource_inventoried_as_id: res.id) |> repo.update() 195 end) 196 |> Repo.transaction() 197 |> case do 198 {:ok, res} -> {:ok, res} 199 {:error, op, val, chng} -> {:error, op, val, chng} 200 end 201 end) 202 203 if Enum.all?(result, &match?({:ok, _}, &1)) do 204 {:ok, result} 205 else 206 {:error, result} 207 end 208 else 209 {:ok, {{_, stat, _}, _, body_charlist}} -> 210 {:error, "the http call result in non-200 status code #{stat}: #{to_string(body_charlist)}"} 211 212 other -> other 213 end 214 end 215 216 @spec project_agents(String.t()) :: [Agent.t()] 217 def project_agents(url) do 218 import Ecto.Query 219 220 from(p in Process, 221 join: e in assoc(p, :inputs), 222 join: a in assoc(e, :provider), 223 where: p.name == ^url, 224 select: a) 225 |> Repo.all() 226 end 227 228 # Get emails from the softwarepassport repositories output. 229 @spec get_emails(map()) :: %{String.t() => MapSet.t()} 230 defp get_emails(map) do 231 Enum.reduce(map, %{}, fn p, acc -> 232 # the scanning might be in proccess, so it might be `nil` 233 if p["scancode_report"] do 234 emails = 235 Enum.reduce(p["scancode_report"]["files"], MapSet.new(), fn f, acc -> 236 Enum.reduce(f["emails"], acc, fn e, acc -> 237 MapSet.put(acc, String.downcase(e["email"])) 238 end) 239 end) 240 if emails == MapSet.new() do # empty? 241 acc 242 else 243 Map.put(acc, p["url"], emails) 244 end 245 else 246 acc 247 end 248 end) 249 end 250 251 # Return the useragent to be used by the HTTP client, this module. 252 @spec useragent() :: charlist() 253 defp useragent() do 254 'zenflows/' ++ Application.spec(:zenflows, :vsn) 255 end 256 end