1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
7 A module to handle coding from internal to wire ActivityPub and back.
11 alias Pleroma.Object.Containment
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
23 Modifies an incoming AP object (mastodon format) to our internal format.
25 def fix_object(object) do
41 def fix_summary(%{"summary" => nil} = object) do
43 |> Map.put("summary", "")
46 def fix_summary(%{"summary" => _} = object) do
47 # summary is present, nothing to do
51 def fix_summary(object) do
53 |> Map.put("summary", "")
56 def fix_addressing_list(map, field) do
58 is_binary(map[field]) ->
59 Map.put(map, field, [map[field]])
62 Map.put(map, field, [])
69 def fix_explicit_addressing(
70 %{"to" => to, "cc" => cc} = object,
76 |> Enum.filter(fn x -> x in explicit_mentions end)
80 |> Enum.filter(fn x -> x not in explicit_mentions end)
84 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
88 |> Map.put("to", explicit_to)
89 |> Map.put("cc", final_cc)
92 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
94 # if directMessage flag is set to true, leave the addressing alone
95 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
97 def fix_explicit_addressing(object) do
100 |> Utils.determine_explicit_mentions()
102 follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
105 explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection]
107 fix_explicit_addressing(object, explicit_mentions, follower_collection)
110 # if as:Public is addressed, then make sure the followers collection is also addressed
111 # so that the activities will be delivered to local users.
112 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
113 recipients = to ++ cc
115 if followers_collection not in recipients do
117 "https://www.w3.org/ns/activitystreams#Public" in cc ->
118 to = to ++ [followers_collection]
119 Map.put(object, "to", to)
121 "https://www.w3.org/ns/activitystreams#Public" in to ->
122 cc = cc ++ [followers_collection]
123 Map.put(object, "cc", cc)
133 def fix_implicit_addressing(object, _), do: object
135 def fix_addressing(object) do
136 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
137 followers_collection = User.ap_followers(user)
140 |> fix_addressing_list("to")
141 |> fix_addressing_list("cc")
142 |> fix_addressing_list("bto")
143 |> fix_addressing_list("bcc")
144 |> fix_explicit_addressing()
145 |> fix_implicit_addressing(followers_collection)
148 def fix_actor(%{"attributedTo" => actor} = object) do
150 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
153 # Check for standardisation
154 # This is what Peertube does
155 # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
156 # Prismo returns only an integer (count) as "likes"
157 def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
159 |> Map.put("likes", [])
160 |> Map.put("like_count", 0)
163 def fix_likes(object) do
167 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
168 when not is_nil(in_reply_to) do
171 is_bitstring(in_reply_to) ->
174 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
177 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
178 Enum.at(in_reply_to, 0)
180 # Maybe I should output an error too?
185 case get_obj_helper(in_reply_to_id) do
186 {:ok, replied_object} ->
187 with %Activity{} = _activity <-
188 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
190 |> Map.put("inReplyTo", replied_object.data["id"])
191 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
192 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
193 |> Map.put("context", replied_object.data["context"] || object["conversation"])
196 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
201 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
206 def fix_in_reply_to(object), do: object
208 def fix_context(object) do
209 context = object["context"] || object["conversation"] || Utils.generate_context_id()
212 |> Map.put("context", context)
213 |> Map.put("conversation", context)
216 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
219 |> Enum.map(fn data ->
220 media_type = data["mediaType"] || data["mimeType"]
221 href = data["url"] || data["href"]
223 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
226 |> Map.put("mediaType", media_type)
227 |> Map.put("url", url)
231 |> Map.put("attachment", attachments)
234 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
235 Map.put(object, "attachment", [attachment])
239 def fix_attachments(object), do: object
241 def fix_url(%{"url" => url} = object) when is_map(url) do
243 |> Map.put("url", url["href"])
246 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
247 first_element = Enum.at(url, 0)
251 |> Enum.filter(fn x -> is_map(x) end)
252 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
256 |> Map.put("attachment", [first_element])
257 |> Map.put("url", link_element["href"])
260 def fix_url(%{"type" => object_type, "url" => url} = object)
261 when object_type != "Video" and is_list(url) do
262 first_element = Enum.at(url, 0)
266 is_bitstring(first_element) -> first_element
267 is_map(first_element) -> first_element["href"] || ""
272 |> Map.put("url", url_string)
275 def fix_url(object), do: object
277 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
278 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
282 |> Enum.reduce(%{}, fn data, mapping ->
283 name = String.trim(data["name"], ":")
285 mapping |> Map.put(name, data["icon"]["url"])
288 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
289 emoji = Map.merge(object["emoji"] || %{}, emoji)
292 |> Map.put("emoji", emoji)
295 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
296 name = String.trim(tag["name"], ":")
297 emoji = %{name => tag["icon"]["url"]}
300 |> Map.put("emoji", emoji)
303 def fix_emoji(object), do: object
305 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
308 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
309 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
311 combined = tag ++ tags
314 |> Map.put("tag", combined)
317 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
318 combined = [tag, String.slice(hashtag, 1..-1)]
321 |> Map.put("tag", combined)
324 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
326 def fix_tag(object), do: object
328 # content map usually only has one language so this will do for now.
329 def fix_content_map(%{"contentMap" => content_map} = object) do
330 content_groups = Map.to_list(content_map)
331 {_, content} = Enum.at(content_groups, 0)
334 |> Map.put("content", content)
337 def fix_content_map(object), do: object
339 def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do
340 reply = Object.normalize(reply_id)
342 if reply.data["type"] == "Question" and object["name"] do
343 Map.put(object, "type", "Answer")
349 def fix_type(object), do: object
351 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
352 with true <- id =~ "follows",
353 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
354 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
361 defp mastodon_follow_hack(_, _), do: {:error, nil}
363 defp get_follow_activity(follow_object, followed) do
364 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
365 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
368 # Can't find the activity. This might a Mastodon 2.3 "Accept"
370 mastodon_follow_hack(follow_object, followed)
377 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
379 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
380 with context <- data["context"] || Utils.generate_context_id(),
381 content <- data["content"] || "",
382 %User{} = actor <- User.get_cached_by_ap_id(actor),
384 # Reduce the object list to find the reported user.
386 Enum.reduce_while(objects, nil, fn ap_id, _ ->
387 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
394 # Remove the reported user from the object list.
395 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
403 "cc" => [account.ap_id]
407 ActivityPub.flag(params)
411 # disallow objects with bogus IDs
412 def handle_incoming(%{"id" => nil}), do: :error
413 def handle_incoming(%{"id" => ""}), do: :error
414 # length of https:// = 8, should validate better, but good enough for now.
415 def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
417 # TODO: validate those with a Ecto scheme
420 def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
421 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
422 actor = Containment.get_actor(data)
425 Map.put(data, "actor", actor)
428 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
429 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
430 object = fix_object(data["object"])
436 context: object["conversation"],
438 published: data["published"],
447 ActivityPub.create(params)
449 %Activity{} = activity -> {:ok, activity}
455 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
457 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
458 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
459 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
460 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
461 {:user_blocked, false} <-
462 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
463 {:user_locked, false} <- {:user_locked, User.locked?(followed)},
464 {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
465 ActivityPub.accept(%{
466 to: [follower.ap_id],
472 {:user_blocked, true} ->
473 {:ok, _} = Utils.update_follow_state(activity, "reject")
475 ActivityPub.reject(%{
476 to: [follower.ap_id],
482 {:follow, {:error, _}} ->
483 {:ok, _} = Utils.update_follow_state(activity, "reject")
485 ActivityPub.reject(%{
486 to: [follower.ap_id],
492 {:user_locked, true} ->
504 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
506 with actor <- Containment.get_actor(data),
507 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
508 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
509 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
510 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
512 ActivityPub.accept(%{
513 to: follow_activity.data["to"],
516 object: follow_activity.data["id"],
519 if not User.following?(follower, followed) do
520 {:ok, _follower} = User.follow(follower, followed)
530 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
532 with actor <- Containment.get_actor(data),
533 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
534 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
535 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
536 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
538 ActivityPub.reject(%{
539 to: follow_activity.data["to"],
542 object: follow_activity.data["id"],
545 User.unfollow(follower, followed)
554 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
556 with actor <- Containment.get_actor(data),
557 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
558 {:ok, object} <- get_obj_helper(object_id),
559 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
567 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
569 with actor <- Containment.get_actor(data),
570 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
571 {:ok, object} <- get_obj_helper(object_id),
572 public <- Visibility.is_public?(data),
573 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
581 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
584 when object_type in ["Person", "Application", "Service", "Organization"] do
585 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
586 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
588 banner = new_user_data[:info]["banner"]
589 locked = new_user_data[:info]["locked"] || false
593 |> Map.take([:name, :bio, :avatar])
594 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
597 |> User.upgrade_changeset(update_data)
598 |> User.update_and_set_cache()
600 ActivityPub.update(%{
602 to: data["to"] || [],
603 cc: data["cc"] || [],
614 # TODO: We presently assume that any actor on the same origin domain as the object being
615 # deleted has the rights to delete that object. A better way to validate whether or not
616 # the object should be deleted is to refetch the object URI, which should return either
617 # an error or a tombstone. This would allow us to verify that a deletion actually took
620 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
622 object_id = Utils.get_ap_id(object_id)
624 with actor <- Containment.get_actor(data),
625 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
626 {:ok, object} <- get_obj_helper(object_id),
627 :ok <- Containment.contain_origin(actor.ap_id, object.data),
628 {:ok, activity} <- ActivityPub.delete(object, false) do
638 "object" => %{"type" => "Announce", "object" => object_id},
643 with actor <- Containment.get_actor(data),
644 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
645 {:ok, object} <- get_obj_helper(object_id),
646 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
656 "object" => %{"type" => "Follow", "object" => followed},
661 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
662 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
663 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
664 User.unfollow(follower, followed)
674 "object" => %{"type" => "Block", "object" => blocked},
679 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
680 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
681 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
682 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
683 User.unblock(blocker, blocked)
691 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
693 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
694 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
695 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
696 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
697 User.unfollow(blocker, blocked)
698 User.block(blocker, blocked)
708 "object" => %{"type" => "Like", "object" => object_id},
713 with actor <- Containment.get_actor(data),
714 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
715 {:ok, object} <- get_obj_helper(object_id),
716 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
723 def handle_incoming(_), do: :error
725 def get_obj_helper(id) do
726 if object = Object.normalize(id), do: {:ok, object}, else: nil
729 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
730 with false <- String.starts_with?(in_reply_to, "http"),
731 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
732 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
738 def set_reply_to_uri(obj), do: obj
740 # Prepares the object of an outgoing create activity.
741 def prepare_object(object) do
749 |> prepare_attachments
752 |> strip_internal_fields
753 |> strip_internal_tags
759 # internal -> Mastodon
762 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
764 Object.normalize(object_id).data
769 |> Map.put("object", object)
770 |> Map.merge(Utils.make_json_ld_header())
775 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
776 # because of course it does.
777 def prepare_outgoing(%{"type" => "Accept"} = data) do
778 with follow_activity <- Activity.normalize(data["object"]) do
780 "actor" => follow_activity.actor,
781 "object" => follow_activity.data["object"],
782 "id" => follow_activity.data["id"],
788 |> Map.put("object", object)
789 |> Map.merge(Utils.make_json_ld_header())
795 def prepare_outgoing(%{"type" => "Reject"} = data) do
796 with follow_activity <- Activity.normalize(data["object"]) do
798 "actor" => follow_activity.actor,
799 "object" => follow_activity.data["object"],
800 "id" => follow_activity.data["id"],
806 |> Map.put("object", object)
807 |> Map.merge(Utils.make_json_ld_header())
813 def prepare_outgoing(%{"type" => _type} = data) do
816 |> strip_internal_fields
817 |> maybe_fix_object_url
818 |> Map.merge(Utils.make_json_ld_header())
823 def maybe_fix_object_url(data) do
824 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
825 case get_obj_helper(data["object"]) do
826 {:ok, relative_object} ->
827 if relative_object.data["external_url"] do
830 |> Map.put("object", relative_object.data["external_url"])
836 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
844 def add_hashtags(object) do
846 (object["tag"] || [])
848 # Expand internal representation tags into AS2 tags.
849 tag when is_binary(tag) ->
851 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
856 # Do not process tags which are already AS2 tag objects.
857 tag when is_map(tag) ->
862 |> Map.put("tag", tags)
865 def add_mention_tags(object) do
868 |> Utils.get_notified_from_object()
869 |> Enum.map(fn user ->
870 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
873 tags = object["tag"] || []
876 |> Map.put("tag", tags ++ mentions)
879 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
880 user_info = add_emoji_tags(user_info)
883 |> Map.put(:info, user_info)
886 # TODO: we should probably send mtime instead of unix epoch time for updated
887 def add_emoji_tags(%{"emoji" => emoji} = object) do
888 tags = object["tag"] || []
892 |> Enum.map(fn {name, url} ->
894 "icon" => %{"url" => url, "type" => "Image"},
895 "name" => ":" <> name <> ":",
897 "updated" => "1970-01-01T00:00:00Z",
903 |> Map.put("tag", tags ++ out)
906 def add_emoji_tags(object) do
910 def set_conversation(object) do
911 Map.put(object, "conversation", object["context"])
914 def set_sensitive(object) do
915 tags = object["tag"] || []
916 Map.put(object, "sensitive", "nsfw" in tags)
919 def set_type(%{"type" => "Answer"} = object) do
920 Map.put(object, "type", "Note")
923 def set_type(object), do: object
925 def add_attributed_to(object) do
926 attributed_to = object["attributedTo"] || object["actor"]
929 |> Map.put("attributedTo", attributed_to)
932 def add_likes(%{"id" => id, "like_count" => likes} = object) do
934 "id" => "#{id}/likes",
935 "first" => "#{id}/likes?page=1",
936 "type" => "OrderedCollection",
937 "totalItems" => likes
941 |> Map.put("likes", likes)
944 def add_likes(object) do
948 def prepare_attachments(object) do
950 (object["attachment"] || [])
951 |> Enum.map(fn data ->
952 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
953 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
957 |> Map.put("attachment", attachments)
960 defp strip_internal_fields(object) do
965 "announcement_count",
968 "deleted_activity_id"
972 defp strip_internal_tags(%{"tag" => tags} = object) do
975 |> Enum.filter(fn x -> is_map(x) end)
978 |> Map.put("tag", tags)
981 defp strip_internal_tags(object), do: object
983 def perform(:user_upgrade, user) do
984 # we pass a fake user so that the followers collection is stripped away
985 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
990 where: ^old_follower_address in u.following,
995 "array_replace(?,?,?)",
997 ^old_follower_address,
998 ^user.follower_address
1004 Repo.update_all(q, [])
1006 maybe_retire_websub(user.ap_id)
1011 where: ^old_follower_address in a.recipients,
1016 "array_replace(?,?,?)",
1018 ^old_follower_address,
1019 ^user.follower_address
1025 Repo.update_all(q, [])
1028 def upgrade_user_from_ap_id(ap_id) do
1029 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1030 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1031 already_ap <- User.ap_enabled?(user),
1032 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1033 unless already_ap do
1034 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1039 %User{} = user -> {:ok, user}
1044 def maybe_retire_websub(ap_id) do
1045 # some sanity checks
1046 if is_binary(ap_id) && String.length(ap_id) > 8 do
1049 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1050 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1057 def maybe_fix_user_url(data) do
1058 if is_map(data["url"]) do
1059 Map.put(data, "url", data["url"]["href"])
1065 def maybe_fix_user_object(data) do
1067 |> maybe_fix_user_url