Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a254079
Revert "removed poly references" 1e85db47f6d99d983e7198a9c55f04c11eb3…
andybak Aug 30, 2024
a435ae2
Replacing hardcoded references to poly.googleapis.com with icosa-api.…
Craigmoore Aug 30, 2024
a496cfd
Poly to Icosa
andybak Aug 30, 2024
85bb2d0
Update media_resolver.ex
andybak Aug 30, 2024
aa39618
Replacing MediaSearchQuery to not check that the google_poly api key …
Craigmoore Sep 6, 2024
ee8ce21
remove api key from single asset api call
andybak Sep 6, 2024
08f741e
switch resolver to api instead of front end urls
andybak Sep 13, 2024
dd7eed2
only ask for GLTF2
andybak Sep 13, 2024
8069c31
Back to front end url but add root to non_video_root_hosts
andybak Sep 13, 2024
3cea842
Sigh. Back to api urls
andybak Sep 13, 2024
2e82b1f
Try moving all urls to be api urls
andybak Sep 13, 2024
8bae0a6
quick fix for ".co.uk"
andybak Sep 13, 2024
525a1a0
Minor fixes for resolving icosa assets
Craigmoore Sep 27, 2024
c8b356e
Adding temporary fix for icosa asset url
Craigmoore Sep 27, 2024
b9cd448
Merge branch 'Hubs-Foundation:master' into master
Craigmoore Oct 25, 2024
5a1b0a7
Remove references to google_poly_api_key
andybak Oct 25, 2024
d6fe31e
rename another poly to icosa
andybak Oct 25, 2024
59e8127
More poly > icosa renaming
andybak Oct 25, 2024
cb896bb
Switch from staging to production urls
andybak Nov 13, 2024
4343a30
Revert "quick fix for ".co.uk""
andybak Nov 13, 2024
026528c
Merge branch 'Hubs-Foundation:master' into master
andybak May 14, 2025
c975619
Merge branch 'Hubs-Foundation:master' into master
andybak Sep 5, 2025
bfadcb4
Only show remixable and order by "best"
andybak Sep 5, 2025
653df22
Remove empty category param
andybak Sep 5, 2025
05143e2
Handle redirects in the CORS proxy
andybak Sep 8, 2025
a636aed
mix format
andybak Sep 10, 2025
fc784c4
Revert "mix format"
andybak Sep 11, 2025
8acf9cf
Reverts Icosa to use usual caching period, and reverts a comment
andybak Nov 12, 2025
facd9be
Mix format
andybak Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/ret/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ defmodule Ret.Account do
_ -> false
end

Repo.insert!(%Account{login: %Login{identifier_hash: identifier_hash}, is_admin: is_admin})
Repo.insert!(%Account{
login: %Login{identifier_hash: identifier_hash},
is_admin: is_admin
})

true ->
nil
Expand Down
81 changes: 52 additions & 29 deletions lib/ret/discord_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,23 @@ defmodule Ret.DiscordClient do
{status, result} when status in [:commit, :ok] -> "#{result["nick"]}"
end

nickname = if !nickname or nickname == "" do
case Cachex.fetch(:discord_api, "/users/#{provider_account_id}") do
{status, result} when status in [:commit, :ok] -> "#{result["global_name"]}"
end
nickname =
if !nickname or nickname == "" do
case Cachex.fetch(:discord_api, "/users/#{provider_account_id}") do
{status, result} when status in [:commit, :ok] -> "#{result["global_name"]}"
end
else
nickname
end

nickname = if !nickname or nickname == "" do
case Cachex.fetch(:discord_api, "/users/#{provider_account_id}") do
{status, result} when status in [:commit, :ok] -> "#{result["username"]}"
end

nickname =
if !nickname or nickname == "" do
case Cachex.fetch(:discord_api, "/users/#{provider_account_id}") do
{status, result} when status in [:commit, :ok] -> "#{result["username"]}"
end
else
nickname
end
end
end

def fetch_community_identifier(%Ret.OAuthProvider{
Expand Down Expand Up @@ -169,18 +171,21 @@ defmodule Ret.DiscordClient do
case Cachex.fetch(:discord_api, "/guilds/#{community_id}/roles") do
{status, result} when status in [:commit, :ok] -> result |> Map.new(&{&1["id"], &1})
end
# Note: Whether the bitfield values in guild_roles are represented as strings or integers is inconsistent (possibly based on what permissions the user has), so every time they're used they need to be checked and, if needed, converted to integers.

# Note: Whether the bitfield values in guild_roles are represented as strings or integers is inconsistent (possibly based on what permissions the user has), so every time they're used they need to be checked and, if needed, converted to integers.

role_everyone = guild_roles[community_id]
permissions = role_everyone["permissions"]

user_permissions = user_roles |> Enum.map(&guild_roles[&1]["permissions"])

permissions = user_permissions |>
Enum.reduce(permissions, &(
(if is_binary(&1), do: String.to_integer(&1), else: &1) |||
(if is_binary(&2), do: String.to_integer(&2), else: &2)
))
permissions =
user_permissions
|> Enum.reduce(
permissions,
&(if(is_binary(&1), do: String.to_integer(&1), else: &1) |||
if(is_binary(&2), do: String.to_integer(&2), else: &2))
)

if (permissions &&& @administrator) == @administrator do
@all
Expand All @@ -203,15 +208,21 @@ defmodule Ret.DiscordClient do
|> Map.get("permission_overwrites")
|> Map.new(&{&1["id"], &1})
end
# Note: Whether the bitfield values in channel_overwrites are represented as strings or integers is inconsistent (possibly based on what permissions the user has), so every time they're used they need to be checked and, if needed, converted to integers.

# Note: Whether the bitfield values in channel_overwrites are represented as strings or integers is inconsistent (possibly based on what permissions the user has), so every time they're used they need to be checked and, if needed, converted to integers.

overwrite_everyone = channel_overwrites[community_id]

permissions =
if overwrite_everyone do
(permissions &&&
~~~(if is_binary(overwrite_everyone["deny"]), do: String.to_integer(overwrite_everyone["deny"]), else: overwrite_everyone["deny"])) |||
(if is_binary(overwrite_everyone["allow"]), do: String.to_integer(overwrite_everyone["allow"]), else: overwrite_everyone["allow"])
~~~if(is_binary(overwrite_everyone["deny"]),
do: String.to_integer(overwrite_everyone["deny"]),
else: overwrite_everyone["deny"]
)) |||
if is_binary(overwrite_everyone["allow"]),
do: String.to_integer(overwrite_everyone["allow"]),
else: overwrite_everyone["allow"]
else
permissions
end
Expand All @@ -220,14 +231,21 @@ defmodule Ret.DiscordClient do
user_permissions =
user_roles |> Enum.map(&channel_overwrites[&1]) |> Enum.filter(&(&1 != nil))

allow = user_permissions |> Enum.reduce(@none, &(
(if is_binary(&1["allow"]), do: String.to_integer(&1["allow"]), else: &1["allow"]) |||
&2
))
deny = user_permissions |> Enum.reduce(@none, &(
(if is_binary(&1["deny"]), do: String.to_integer(&1["deny"]), else: &1["deny"]) |||
&2
))
allow =
user_permissions
|> Enum.reduce(
@none,
&(if(is_binary(&1["allow"]), do: String.to_integer(&1["allow"]), else: &1["allow"]) |||
&2)
)

deny =
user_permissions
|> Enum.reduce(
@none,
&(if(is_binary(&1["deny"]), do: String.to_integer(&1["deny"]), else: &1["deny"]) |||
&2)
)

permissions = (permissions &&& ~~~deny) ||| allow

Expand All @@ -237,8 +255,13 @@ defmodule Ret.DiscordClient do
permissions =
if overwrite_member do
(permissions &&&
~~~(if is_binary(overwrite_member["deny"]), do: String.to_integer(overwrite_member["deny"]), else: overwrite_member["deny"])) |||
(if is_binary(overwrite_member["allow"]), do: String.to_integer(overwrite_member["allow"]), else: overwrite_member["allow"])
~~~if(is_binary(overwrite_member["deny"]),
do: String.to_integer(overwrite_member["deny"]),
else: overwrite_member["deny"]
)) |||
if is_binary(overwrite_member["allow"]),
do: String.to_integer(overwrite_member["allow"]),
else: overwrite_member["allow"]
else
permissions
end
Expand Down
15 changes: 8 additions & 7 deletions lib/ret/http_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,19 @@ defmodule Ret.HttpUtils do
end

def join_smart(enum) do
Enum.reduce(enum, "", fn(x, acc) ->
x = cond do
!x -> nil
is_binary(x) -> String.trim(x)
true -> "#{x}"
end
Enum.reduce(enum, "", fn x, acc ->
x =
cond do
!x -> nil
is_binary(x) -> String.trim(x)
true -> "#{x}"
end

if x && x != "" do
if acc && acc != "", do: acc <> " — " <> x, else: x
else
acc
end
end)
end

end
51 changes: 50 additions & 1 deletion lib/ret/media_resolver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ defmodule Ret.MediaResolver do

@youtube_rate_limit %{scale: 8_000, limit: 1}
@sketchfab_rate_limit %{scale: 60_000, limit: 15}
@icosa_rate_limit %{scale: 60_000, limit: 1000}
@max_await_for_rate_limit_s 120

@non_video_root_hosts [
"sketchfab.com",
"giphy.com",
"tenor.com"
"tenor.com",
"icosa.gallery"
]

@deviant_id_regex ~r/\"DeviantArt:\/\/deviation\/([^"]+)/
Expand Down Expand Up @@ -79,6 +81,13 @@ defmodule Ret.MediaResolver do
|> resolved()}
end

# Necessary short circuit around google.com root_host to skip YT-DL check for Poly
Copy link
Member

@DougReeder DougReeder Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there isn't a rate limit for the Icosa API, is this code needed?

def resolve(%MediaResolverQuery{url: %URI{host: "api.icosa.gallery"}} = query, root_host) do
rate_limited_resolve(query, root_host, @icosa_rate_limit, fn ->
resolve_non_video(query, root_host)
end)
end

def resolve(%MediaResolverQuery{} = query, root_host) when root_host in @non_video_root_hosts do
resolve_non_video(query, root_host)
end
Expand Down Expand Up @@ -327,6 +336,46 @@ defmodule Ret.MediaResolver do
{:commit, (resolved_url || uri) |> resolved(meta)}
end

defp resolve_non_video(
%MediaResolverQuery{
url: %URI{host: "api.icosa.gallery", path: "/v1/assets/" <> asset_id} = uri
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value assigned to uri is not used. If this is intentional, we should drop = uri

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... or we could rename uri to _uri to indicate it's not used.

},
"icosa.gallery"
) do
# Increment stat for the request
Statix.increment("ret.media_resolver.icosa.requests")

# Make the API call to get the asset data
payload =
"https://api.icosa.gallery/v1/assets/#{asset_id}"
# Assuming this function sends the request and handles retries
|> retry_get_until_success()
|> Map.get(:body)
|> Poison.decode!()

# Create the meta information based on the payload
meta =
%{
expected_content_type: "model/gltf",
name: payload["displayName"],
author: payload["authorName"],
license: payload["license"]
}

# Extract the GLTF2 format URL from the payload
uri =
payload["formats"]
|> Enum.find(&(&1["formatType"] == "GLTF2"))
|> Kernel.get_in(["root", "url"])
|> URI.parse()

# Increment stat for successful resolution
Statix.increment("ret.media_resolver.icosa.ok")

# Return the URI and meta data for further processing
{:commit, resolved(uri, meta)}
end

defp resolve_non_video(
%MediaResolverQuery{url: %URI{path: "/models/" <> model_id}} = query,
"sketchfab.com" = root_host
Expand Down
63 changes: 62 additions & 1 deletion lib/ret/media_search.ex
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,53 @@ defmodule Ret.MediaSearch do
sketchfab_search(query)
end

def search(%Ret.MediaSearchQuery{source: "youtube_videos", cursor: cursor, filter: filter, q: q}) do
def search(%Ret.MediaSearchQuery{source: "icosa", cursor: cursor, filter: filter, q: q}) do
query =
URI.encode_query(
pageSize: @page_size,
maxComplexity: :MEDIUM,
format: :GLTF2,
pageToken: cursor,
category: filter,
licence: :REMIXABLE,
orderBy: :BEST,
keywords: q
)

url = "https://api.icosa.gallery/v1/assets?#{query}"
# Remove empty category queries as the Icosa API returns validation errors instead of "any"
url = String.replace(url, "category=&", "")

res =
url
|> retry_get_until_success()

case res do
:error ->
:error

res ->
decoded_res = res |> Map.get(:body) |> Poison.decode!()
entries = decoded_res |> Map.get("assets") |> Enum.map(&icosa_api_result_to_entry/1)
next_cursor = decoded_res |> Map.get("nextPageToken")

{:commit,
%Ret.MediaSearchResult{
meta: %Ret.MediaSearchResultMeta{
next_cursor: next_cursor,
source: :icosa
},
entries: entries
}}
end
end

def search(%Ret.MediaSearchQuery{
source: "youtube_videos",
cursor: cursor,
filter: filter,
q: q
}) do
with api_key when is_binary(api_key) <- resolver_config(:youtube_api_key) do
query =
URI.encode_query(
Expand Down Expand Up @@ -377,6 +423,8 @@ defmodule Ret.MediaSearch do
end
end

# Icosa does not currently require an API key
def available?(:icosa), do: true
def available?(:bing_images), do: has_resolver_config?(:bing_search_api_key)
def available?(:bing_videos), do: has_resolver_config?(:bing_search_api_key)
def available?(:youtube_videos), do: has_resolver_config?(:youtube_api_key)
Expand Down Expand Up @@ -559,6 +607,7 @@ defmodule Ret.MediaSearch do

defp created_rooms_search(cursor, account_id, _query) do
page_number = (cursor || "1") |> Integer.parse() |> elem(0)

ecto_query =
from h in Hub,
where: h.created_by_account_id == ^account_id,
Expand Down Expand Up @@ -957,6 +1006,18 @@ defmodule Ret.MediaSearch do
}
end

defp icosa_api_result_to_entry(result) do
%{
id: result["assetId"],
type: "icosa_model",
name: result["displayName"],
attributions: %{creator: %{name: result["authorName"]}},
url: "http://api.icosa.gallery/v1/assets/" <> result["assetId"],
# url: result["url"],
images: %{preview: %{url: result["thumbnail"]["url"]}}
}
end

defp youtube_api_result_to_entry(result) do
%{
id: result["id"]["videoId"],
Expand Down
1 change: 1 addition & 0 deletions lib/ret/meta.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ defmodule Ret.Meta do
def available_integrations_meta do
%{
twitter: Ret.TwitterClient.available?(),
icosa: Ret.MediaSearch.available?(:icosa),
bing_images: Ret.MediaSearch.available?(:bing_images),
bing_videos: Ret.MediaSearch.available?(:bing_videos),
youtube_videos: Ret.MediaSearch.available?(:youtube_videos),
Expand Down
11 changes: 6 additions & 5 deletions lib/ret/scene.ex
Original file line number Diff line number Diff line change
Expand Up @@ -226,37 +226,38 @@ defmodule Ret.Scene do
model_owned_file: old_model_owned_file,
account: account
} = scene

new_scene_owned_file =
Storage.create_new_owned_file_with_replaced_string(
old_scene_owned_file,
account,
old_domain_url,
new_domain_url
)

{:ok, new_model_owned_file} =
Storage.duplicate_and_transform(old_model_owned_file, account, fn glb_stream,
_total_bytes ->
GLTFUtils.replace_in_glb(glb_stream, old_domain_url, new_domain_url)
end)

scene
|> change()
|> put_change(:scene_owned_file_id, new_scene_owned_file.owned_file_id)
|> put_change(:model_owned_file_id, new_model_owned_file.owned_file_id)
|> Repo.update!()

for old_owned_file <- [old_scene_owned_file, old_model_owned_file] do
OwnedFile.set_inactive(old_owned_file)
Storage.rm_files_for_owned_file(old_owned_file)
Repo.delete(old_owned_file)
end
rescue
e ->
e ->
IO.warn("Failed to process scene due to an error: #{inspect(e)}")
end
end)

:ok
end)
end
Expand Down
Loading