Merge branch 'event/create-with-address' into 'master'
Add address input and refactor federation stuff See merge request framasoft/mobilizon!163
This commit is contained in:
commit
3fa2bd35d8
@ -50,6 +50,8 @@ config :mobilizon, MobilizonWeb.Endpoint,
|
||||
# Do not include metadata nor timestamps in development logs
|
||||
config :logger, :console, format: "[$level] $message\n", level: :debug
|
||||
|
||||
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
config :phoenix, :stacktrace_depth, 20
|
||||
|
53
js/src/components/Event/AddressAutoComplete.vue
Normal file
53
js/src/components/Event/AddressAutoComplete.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<b-field label="Find an address">
|
||||
<b-autocomplete
|
||||
:data="data"
|
||||
placeholder="e.g. 10 Rue Jangot"
|
||||
field="description"
|
||||
:loading="isFetching"
|
||||
@typing="getAsyncData"
|
||||
@select="option => selected = option">
|
||||
|
||||
<template slot-scope="{option}">
|
||||
<b>{{ option.description }}</b>
|
||||
<p>
|
||||
<small>{{ option.street }}</small>, 
|
||||
<small>{{ option.locality }}</small>
|
||||
</p>
|
||||
</template>
|
||||
</b-autocomplete>
|
||||
</b-field>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { IAddress } from '@/types/address.model';
|
||||
import { ADDRESS } from '@/graphql/address';
|
||||
@Component
|
||||
export default class AddressAutoComplete extends Vue {
|
||||
|
||||
@Prop({ required: false, default: () => [] }) initialData!: IAddress[];
|
||||
|
||||
data: IAddress[] = this.initialData;
|
||||
selected: IAddress|null = null;
|
||||
isFetching: boolean = false;
|
||||
|
||||
async getAsyncData(query) {
|
||||
if (!query.length) {
|
||||
this.data = [];
|
||||
return;
|
||||
}
|
||||
this.isFetching = true;
|
||||
const result = await this.$apollo.query({
|
||||
query: ADDRESS,
|
||||
variables: { query },
|
||||
});
|
||||
|
||||
this.data = result.data.searchAddress as IAddress[];
|
||||
}
|
||||
|
||||
@Watch("selected")
|
||||
updateSelected() {
|
||||
this.$emit('input', this.selected);
|
||||
}
|
||||
}
|
||||
</script>
|
18
js/src/graphql/address.ts
Normal file
18
js/src/graphql/address.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const ADDRESS = gql`
|
||||
query($query:String!) {
|
||||
searchAddress(
|
||||
query: $query
|
||||
) {
|
||||
description,
|
||||
geom,
|
||||
floor,
|
||||
street,
|
||||
locality,
|
||||
postalCode,
|
||||
region,
|
||||
country
|
||||
}
|
||||
}
|
||||
`;
|
@ -144,7 +144,8 @@ export const CREATE_EVENT = gql`
|
||||
$category: String!,
|
||||
$beginsOn: DateTime!,
|
||||
$picture: PictureInput,
|
||||
$tags: [String]
|
||||
$tags: [String],
|
||||
$physicalAddress: AddressInput!
|
||||
) {
|
||||
createEvent(
|
||||
title: $title,
|
||||
@ -153,7 +154,8 @@ export const CREATE_EVENT = gql`
|
||||
organizerActorId: $organizerActorId,
|
||||
category: $category,
|
||||
picture: $picture,
|
||||
tags: $tags
|
||||
tags: $tags,
|
||||
physicalAddress: $physicalAddress
|
||||
) {
|
||||
id,
|
||||
uuid,
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface IAddress {
|
||||
id: number;
|
||||
description: string;
|
||||
floor: string;
|
||||
street: string;
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
<tag-input v-model="event.tags" :data="tags" path="title" />
|
||||
|
||||
<address-auto-complete v-model="event.physicalAddress" />
|
||||
|
||||
<date-time-picker v-model="event.beginsOn" :label="$gettext('Starts on…')" :step="15"/>
|
||||
<date-time-picker v-model="event.endsOn" :label="$gettext('Ends on…')" :step="15" />
|
||||
|
||||
@ -57,9 +59,10 @@ import DateTimePicker from '@/components/Event/DateTimePicker.vue';
|
||||
import TagInput from '@/components/Event/TagInput.vue';
|
||||
import { TAGS } from '@/graphql/tags';
|
||||
import { ITag } from '@/types/tag.model';
|
||||
import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
|
||||
|
||||
@Component({
|
||||
components: { TagInput, DateTimePicker, PictureUpload, Editor },
|
||||
components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor },
|
||||
apollo: {
|
||||
loggedPerson: {
|
||||
query: LOGGED_PERSON,
|
||||
@ -134,9 +137,13 @@ export default class CreateEvent extends Vue {
|
||||
const obj = {
|
||||
organizerActorId: this.loggedPerson.id,
|
||||
beginsOn: this.event.beginsOn.toISOString(),
|
||||
tags: this.event.tags.map((tag: ITag) => tag.title),
|
||||
tags: this.event.tags.map((tag: ITag) => tag.title)
|
||||
};
|
||||
const res = Object.assign({}, this.event, obj);
|
||||
let res = Object.assign({}, this.event, obj);
|
||||
|
||||
if (this.event.physicalAddress) {
|
||||
delete this.event.physicalAddress['__typename'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform picture files
|
||||
|
@ -11,10 +11,10 @@ defmodule Mix.Tasks.Mobilizon.Toot do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
case MobilizonWeb.API.Comments.create_comment(from, content) do
|
||||
{:ok, _} ->
|
||||
{:ok, _, _} ->
|
||||
Mix.shell().info("Tooted")
|
||||
|
||||
{:local_actor, _} ->
|
||||
{:local_actor, _, _} ->
|
||||
Mix.shell().error("Failed to toot.\nActor #{from} doesn't exist")
|
||||
|
||||
_ ->
|
||||
|
@ -6,6 +6,20 @@ defmodule Mobilizon.Addresses.Address do
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events.Event
|
||||
# alias Mobilizon.Actors.Actor
|
||||
@attrs [
|
||||
:description,
|
||||
:floor,
|
||||
:geom,
|
||||
:country,
|
||||
:locality,
|
||||
:region,
|
||||
:postal_code,
|
||||
:street,
|
||||
:url
|
||||
]
|
||||
@required [
|
||||
:url
|
||||
]
|
||||
|
||||
schema "addresses" do
|
||||
field(:country, :string)
|
||||
@ -16,8 +30,8 @@ defmodule Mobilizon.Addresses.Address do
|
||||
field(:geom, Geo.PostGIS.Geometry)
|
||||
field(:postal_code, :string)
|
||||
field(:street, :string)
|
||||
has_one(:event, Event, foreign_key: :physical_address_id)
|
||||
# has_one(:group, Actor)
|
||||
field(:url, :string)
|
||||
has_many(:event, Event, foreign_key: :physical_address_id)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
@ -25,15 +39,15 @@ defmodule Mobilizon.Addresses.Address do
|
||||
@doc false
|
||||
def changeset(%Address{} = address, attrs) do
|
||||
address
|
||||
|> cast(attrs, [
|
||||
:description,
|
||||
:floor,
|
||||
:geom,
|
||||
:country,
|
||||
:locality,
|
||||
:region,
|
||||
:postal_code,
|
||||
:street
|
||||
])
|
||||
|> cast(attrs, @attrs)
|
||||
|> set_url()
|
||||
|> validate_required(@required)
|
||||
end
|
||||
|
||||
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
|
||||
url =
|
||||
Map.get(changes, :url, MobilizonWeb.Endpoint.url() <> "/address/#{Ecto.UUID.generate()}")
|
||||
|
||||
put_change(changeset, :url, url)
|
||||
end
|
||||
end
|
||||
|
@ -50,6 +50,21 @@ defmodule Mobilizon.Addresses do
|
||||
"""
|
||||
def get_address!(id), do: Repo.get!(Address, id)
|
||||
|
||||
@doc """
|
||||
Gets a single address by it's url
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_address_by_url("https://mobilizon.social/addresses/4572")
|
||||
%Address{}
|
||||
|
||||
iex> get_address_by_url("https://mobilizon.social/addresses/099")
|
||||
nil
|
||||
"""
|
||||
def get_address_by_url(url) do
|
||||
Repo.get_by(Address, url: url)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a address.
|
||||
|
||||
@ -163,7 +178,7 @@ defmodule Mobilizon.Addresses do
|
||||
We only look at the description for now, and eventually order by object distance
|
||||
"""
|
||||
@spec search_addresses(String.t(), list()) :: list(Address.t())
|
||||
def search_addresses(search, options) do
|
||||
def search_addresses(search, options \\ []) do
|
||||
limit = Keyword.get(options, :limit, 5)
|
||||
|
||||
query = from(a in Address, where: ilike(a.description, ^"%#{search}%"), limit: ^limit)
|
||||
@ -181,7 +196,7 @@ defmodule Mobilizon.Addresses do
|
||||
do: from(a in query, where: ilike(a.country, ^"%#{country}%")),
|
||||
else: query
|
||||
|
||||
Repo.all(query)
|
||||
if Keyword.get(options, :single, false) == true, do: Repo.one(query), else: Repo.all(query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -84,9 +84,9 @@ defmodule Mobilizon.Events.Event do
|
||||
:online_address,
|
||||
:phone_address,
|
||||
:uuid,
|
||||
:picture_id
|
||||
:picture_id,
|
||||
:physical_address_id
|
||||
])
|
||||
|> cast_assoc(:physical_address)
|
||||
|> validate_required([
|
||||
:title,
|
||||
:begins_on,
|
||||
|
@ -382,7 +382,8 @@ defmodule Mobilizon.Events do
|
||||
|
||||
defp do_create_event(attrs) do
|
||||
with {:ok, %Event{} = event} <- %Event{} |> Event.changeset(attrs) |> Repo.insert(),
|
||||
%Event{} = event <- event |> Repo.preload([:tags, :organizer_actor]),
|
||||
%Event{} = event <-
|
||||
event |> Repo.preload([:tags, :organizer_actor, :physical_address, :picture]),
|
||||
{:has_tags, true, _} <- {:has_tags, Map.has_key?(attrs, "tags"), event} do
|
||||
event
|
||||
|> Ecto.Changeset.change()
|
||||
@ -513,8 +514,10 @@ defmodule Mobilizon.Events do
|
||||
@doc """
|
||||
Get an existing tag or create one
|
||||
"""
|
||||
@spec get_or_create_tag(String.t()) :: {:ok, Tag.t()} | {:error, any()}
|
||||
def get_or_create_tag(title) do
|
||||
@spec get_or_create_tag(map()) :: {:ok, Tag.t()} | {:error, any()}
|
||||
def get_or_create_tag(tag) do
|
||||
"#" <> title = tag["name"]
|
||||
|
||||
case Repo.get_by(Tag, title: title) do
|
||||
%Tag{} = tag ->
|
||||
{:ok, tag}
|
||||
@ -1223,9 +1226,13 @@ defmodule Mobilizon.Events do
|
||||
|
||||
"""
|
||||
def create_comment(attrs \\ %{}) do
|
||||
%Comment{}
|
||||
|> Comment.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
with {:ok, %Comment{} = comment} <-
|
||||
%Comment{}
|
||||
|> Comment.changeset(attrs)
|
||||
|> Repo.insert(),
|
||||
%Comment{} = comment <- Repo.preload(comment, [:actor, :in_reply_to_comment]) do
|
||||
{:ok, comment}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -15,7 +15,8 @@ defmodule MobilizonWeb.API.Comments do
|
||||
|
||||
Creates a comment from an actor and a status
|
||||
"""
|
||||
@spec create_comment(String.t(), String.t(), String.t()) :: {:ok, Activity.t()} | any()
|
||||
@spec create_comment(String.t(), String.t(), String.t()) ::
|
||||
{:ok, Activity.t(), Comment.t()} | any()
|
||||
def create_comment(
|
||||
from_username,
|
||||
status,
|
||||
|
@ -2,6 +2,7 @@ defmodule MobilizonWeb.API.Events do
|
||||
@moduledoc """
|
||||
API for Events
|
||||
"""
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
@ -11,7 +12,7 @@ defmodule MobilizonWeb.API.Events do
|
||||
@doc """
|
||||
Create an event
|
||||
"""
|
||||
@spec create_event(map()) :: {:ok, Activity.t()} | any()
|
||||
@spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
|
||||
def create_event(
|
||||
%{
|
||||
title: title,
|
||||
@ -22,10 +23,9 @@ defmodule MobilizonWeb.API.Events do
|
||||
tags: tags
|
||||
} = args
|
||||
) do
|
||||
require Logger
|
||||
|
||||
with %Actor{url: url} = actor <-
|
||||
Actors.get_local_actor_with_everything(organizer_actor_id),
|
||||
physical_address <- Map.get(args, :physical_address, nil),
|
||||
title <- String.trim(title),
|
||||
visibility <- Map.get(args, :visibility, :public),
|
||||
picture <- Map.get(args, :picture, nil),
|
||||
@ -34,14 +34,12 @@ defmodule MobilizonWeb.API.Events do
|
||||
event <-
|
||||
ActivityPubUtils.make_event_data(
|
||||
url,
|
||||
to,
|
||||
%{to: to, cc: cc},
|
||||
title,
|
||||
content_html,
|
||||
picture,
|
||||
tags,
|
||||
cc,
|
||||
%{begins_on: begins_on},
|
||||
category
|
||||
%{begins_on: begins_on, physical_address: physical_address, category: category}
|
||||
) do
|
||||
ActivityPub.create(%{
|
||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
@ -51,4 +49,15 @@ defmodule MobilizonWeb.API.Events do
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
defp get_physical_address(address_id) when is_number(address_id),
|
||||
do: Addresses.get_address!(address_id)
|
||||
|
||||
defp get_physical_address(address_id) when is_binary(address_id) do
|
||||
with {address_id, ""} <- Integer.parse(address_id) do
|
||||
get_physical_address(address_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_physical_address(nil), do: nil
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ defmodule MobilizonWeb.API.Groups do
|
||||
@doc """
|
||||
Create a group
|
||||
"""
|
||||
@spec create_group(map()) :: {:ok, Activity.t()} | any()
|
||||
@spec create_group(map()) :: {:ok, Activity.t(), Group.t()} | any()
|
||||
def create_group(
|
||||
%{
|
||||
preferred_username: title,
|
||||
|
@ -11,7 +11,7 @@ defmodule MobilizonWeb.Resolvers.Address do
|
||||
Search an address
|
||||
"""
|
||||
@spec search(map(), map(), map()) :: {:ok, list(Address.t())}
|
||||
def search(_parent, %{query: query}, %{context: %{ip: ip}}) do
|
||||
def search(_parent, %{query: query, page: _page, limit: _limit}, %{context: %{ip: ip}}) do
|
||||
country = Geolix.lookup(ip) |> Map.get(:country, nil)
|
||||
|
||||
local_addresses = Task.async(fn -> Addresses.search_addresses(query, country: country) end)
|
||||
|
@ -11,14 +11,10 @@ defmodule MobilizonWeb.Resolvers.Comment do
|
||||
def create_comment(_parent, %{text: comment, actor_username: username}, %{
|
||||
context: %{current_user: %User{} = _user}
|
||||
}) do
|
||||
with {:ok, %Activity{data: %{"object" => %{"type" => "Note"} = object}}} <-
|
||||
with {:ok, %Activity{data: %{"object" => %{"type" => "Note"} = _object}},
|
||||
%Comment{} = comment} <-
|
||||
Comments.create_comment(username, comment) do
|
||||
{:ok,
|
||||
%Comment{
|
||||
text: object["content"],
|
||||
url: object["id"],
|
||||
uuid: object["uuid"]
|
||||
}}
|
||||
{:ok, comment}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -3,6 +3,8 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
Handles the event-related GraphQL calls
|
||||
"""
|
||||
alias Mobilizon.Activity
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Media.Picture
|
||||
@ -190,25 +192,10 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
"""
|
||||
def create_event(_parent, args, %{context: %{current_user: _user}} = _resolution) do
|
||||
with {:ok, args} <- save_attached_picture(args),
|
||||
{:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <-
|
||||
{:ok, args} <- save_physical_address(args),
|
||||
{:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <-
|
||||
MobilizonWeb.API.Events.create_event(args) do
|
||||
res = %{
|
||||
title: object["name"],
|
||||
description: object["content"],
|
||||
uuid: object["uuid"],
|
||||
url: object["id"]
|
||||
}
|
||||
|
||||
res =
|
||||
if Map.has_key?(object, "attachment"),
|
||||
do:
|
||||
Map.put(res, :picture, %{
|
||||
name: object["attachment"] |> hd() |> Map.get("name"),
|
||||
url: object["attachment"] |> hd() |> Map.get("url") |> hd() |> Map.get("href")
|
||||
}),
|
||||
else: res
|
||||
|
||||
{:ok, res}
|
||||
{:ok, event}
|
||||
end
|
||||
end
|
||||
|
||||
@ -237,6 +224,25 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
@spec save_attached_picture(map()) :: {:ok, map()}
|
||||
defp save_attached_picture(args), do: {:ok, args}
|
||||
|
||||
@spec save_physical_address(map()) :: {:ok, map()}
|
||||
defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args) do
|
||||
with %Address{} = address <- Addresses.get_address_by_url(physical_address_url),
|
||||
args <- Map.put(args, :physical_address, address) do
|
||||
{:ok, args}
|
||||
end
|
||||
end
|
||||
|
||||
# @spec save_physical_address(map()) :: {:ok, map()}
|
||||
# defp save_physical_address(%{physical_address: address} = args) do
|
||||
# with {:ok, %Address{} = address} <- Addresses.create_address(address),
|
||||
# args <- Map.put(args, :physical_address, address) do
|
||||
# {:ok, args}
|
||||
# end
|
||||
# end
|
||||
|
||||
@spec save_physical_address(map()) :: {:ok, map()}
|
||||
defp save_physical_address(args), do: {:ok, args}
|
||||
|
||||
@doc """
|
||||
Delete an event
|
||||
"""
|
||||
|
@ -47,20 +47,15 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
:ok,
|
||||
%Activity{
|
||||
data: %{
|
||||
"object" => %{"type" => "Group"} = object
|
||||
"object" => %{"type" => "Group"} = _object
|
||||
}
|
||||
}
|
||||
},
|
||||
%Actor{} = group
|
||||
} <-
|
||||
MobilizonWeb.API.Groups.create_group(args) do
|
||||
{
|
||||
:ok,
|
||||
%Actor{
|
||||
preferred_username: object["preferredUsername"],
|
||||
summary: object["summary"],
|
||||
type: :Group,
|
||||
# uuid: object["uuid"],
|
||||
url: object["id"]
|
||||
}
|
||||
group
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -14,6 +14,7 @@ defmodule MobilizonWeb.Schema.AddressType do
|
||||
field(:region, :string)
|
||||
field(:country, :string)
|
||||
field(:description, :string)
|
||||
field(:url, :string)
|
||||
end
|
||||
|
||||
object :phone_address do
|
||||
@ -26,10 +27,25 @@ defmodule MobilizonWeb.Schema.AddressType do
|
||||
field(:info, :string)
|
||||
end
|
||||
|
||||
input_object :address_input do
|
||||
# Either a full picture object
|
||||
field(:geom, :point, description: "The geocoordinates for the point where this address is")
|
||||
field(:floor, :string, description: "The floor this event is at")
|
||||
field(:street, :string, description: "The address's street name (with number)")
|
||||
field(:locality, :string, description: "The address's locality")
|
||||
field(:postal_code, :string)
|
||||
field(:region, :string)
|
||||
field(:country, :string)
|
||||
field(:description, :string)
|
||||
field(:url, :string)
|
||||
end
|
||||
|
||||
object :address_queries do
|
||||
@desc "Search for an address"
|
||||
field :search_address, type: list_of(:address) do
|
||||
arg(:query, non_null(:string))
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
|
||||
resolve(&Resolvers.Address.search/3)
|
||||
end
|
||||
|
@ -22,7 +22,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
field(:begins_on, :datetime, description: "Datetime for when the event begins")
|
||||
field(:ends_on, :datetime, description: "Datetime for when the event ends")
|
||||
field(:status, :event_status, description: "Status of the event")
|
||||
field(:visibility, :event_visibility, description: "The event's visibility")
|
||||
field(:visibility, :event_visibility, description: "The event's visibility")
|
||||
|
||||
field(:picture, :picture,
|
||||
description: "The event's picture",
|
||||
@ -132,6 +132,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
arg(:phone_address, :string)
|
||||
arg(:organizer_actor_id, non_null(:id))
|
||||
arg(:category, non_null(:string))
|
||||
arg(:physical_address, :address_input)
|
||||
|
||||
resolve(&Event.create_event/3)
|
||||
end
|
||||
|
@ -70,10 +70,11 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
def fetch_object_from_url(url) do
|
||||
Logger.info("Fetching object from url #{url}")
|
||||
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
nil <- Events.get_event_by_url(url),
|
||||
nil <- Events.get_comment_from_url(url),
|
||||
{:error, :actor_not_found} <- Actors.get_actor_by_url(url),
|
||||
with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")},
|
||||
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(url)},
|
||||
{:existing_comment, nil} <- {:existing_comment, Events.get_comment_from_url(url)},
|
||||
{:existing_actor, {:error, :actor_not_found}} <-
|
||||
{:existing_actor, Actors.get_actor_by_url(url)},
|
||||
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
|
||||
HTTPoison.get(
|
||||
url,
|
||||
@ -90,25 +91,32 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"actor" => data["attributedTo"],
|
||||
"object" => data
|
||||
},
|
||||
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
|
||||
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
|
||||
case data["type"] do
|
||||
"Event" ->
|
||||
{:ok, Events.get_event_by_url!(activity.data["object"]["id"])}
|
||||
{:ok, Events.get_event_by_url!(object_url)}
|
||||
|
||||
"Note" ->
|
||||
{:ok, Events.get_comment_full_from_url!(activity.data["object"]["id"])}
|
||||
{:ok, Events.get_comment_full_from_url!(object_url)}
|
||||
|
||||
"Actor" ->
|
||||
{:ok, Actors.get_actor_by_url!(activity.data["object"]["id"], true)}
|
||||
{:ok, Actors.get_actor_by_url!(object_url, true)}
|
||||
|
||||
other ->
|
||||
{:error, other}
|
||||
end
|
||||
else
|
||||
%Event{url: event_url} -> {:ok, Events.get_event_by_url!(event_url)}
|
||||
%Comment{url: comment_url} -> {:ok, Events.get_comment_full_from_url!(comment_url)}
|
||||
%Actor{url: actor_url} -> {:ok, Actors.get_actor_by_url!(actor_url, true)}
|
||||
e -> {:error, e}
|
||||
{:existing_event, %Event{url: event_url}} ->
|
||||
{:ok, Events.get_event_by_url!(event_url)}
|
||||
|
||||
{:existing_comment, %Comment{url: comment_url}} ->
|
||||
{:ok, Events.get_comment_full_from_url!(comment_url)}
|
||||
|
||||
{:existing_actor, %Actor{url: actor_url}} ->
|
||||
{:ok, Actors.get_actor_by_url!(actor_url, true)}
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@ -130,10 +138,10 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
additional
|
||||
),
|
||||
:ok <- Logger.debug(inspect(create_data)),
|
||||
{:ok, activity, _object} <- insert(create_data, local),
|
||||
{:ok, activity, object} <- insert(create_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
# {:ok, actor} <- Actors.increase_event_count(actor) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong")
|
||||
@ -147,9 +155,9 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
local = !(params[:local] == false)
|
||||
|
||||
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
{:ok, activity, object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
@ -164,9 +172,9 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
},
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
{:ok, activity, object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
@ -179,7 +187,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
# ) do
|
||||
# with nil <- get_existing_like(url, object),
|
||||
# like_data <- make_like_data(user, object, activity_id),
|
||||
# {:ok, activity, _object} <- insert(like_data, local),
|
||||
# {:ok, activity, object} <- insert(like_data, local),
|
||||
# {:ok, object} <- add_like_to_object(activity, object),
|
||||
# :ok <- maybe_federate(activity) do
|
||||
# {:ok, activity, object}
|
||||
@ -215,7 +223,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
# ) do
|
||||
# #with true <- is_public?(object),
|
||||
# with announce_data <- make_announce_data(actor, object, activity_id),
|
||||
# {:ok, activity, _object} <- insert(announce_data, local),
|
||||
# {:ok, activity, object} <- insert(announce_data, local),
|
||||
# # {:ok, object} <- add_announce_to_object(activity, object),
|
||||
# :ok <- maybe_federate(activity) do
|
||||
# {:ok, activity, object}
|
||||
@ -250,9 +258,9 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
activity_follow_id <-
|
||||
activity_id || "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity",
|
||||
data <- make_follow_data(followed, follower, activity_follow_id),
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
{:ok, activity, object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{err, _} when err in [:already_following, :suspended] ->
|
||||
{:error, err}
|
||||
@ -269,9 +277,9 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
data <- make_follow_data(followed, follower, follow_id),
|
||||
{:ok, follow_activity, _object} <- insert(data, local),
|
||||
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
|
||||
{:ok, activity, _object} <- insert(unfollow_data, local),
|
||||
{:ok, activity, object} <- insert(unfollow_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
else
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
@ -290,9 +298,9 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
}
|
||||
|
||||
with {:ok, _} <- Events.delete_event(event),
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
{:ok, activity, object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
@ -305,9 +313,9 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
}
|
||||
|
||||
with {:ok, _} <- Events.delete_comment(comment),
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
{:ok, activity, object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
@ -320,9 +328,9 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
}
|
||||
|
||||
with {:ok, _} <- Actors.delete_actor(actor),
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
{:ok, activity, object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
|
58
lib/service/activity_pub/converters/address.ex
Normal file
58
lib/service/activity_pub/converters/address.ex
Normal file
@ -0,0 +1,58 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converters.Address do
|
||||
@moduledoc """
|
||||
Flag converter
|
||||
|
||||
This module allows to convert reports from ActivityStream format to our own internal one, and back.
|
||||
|
||||
Note: Reports are named Flag in AS.
|
||||
"""
|
||||
alias Mobilizon.Addresses.Address, as: AddressModel
|
||||
alias Mobilizon.Service.ActivityPub.Converter
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map()) :: map()
|
||||
def as_to_model_data(object) do
|
||||
res = %{
|
||||
"description" => object["name"],
|
||||
"url" => object["url"]
|
||||
}
|
||||
|
||||
res =
|
||||
if is_nil(object["address"]) do
|
||||
res
|
||||
else
|
||||
Map.merge(res, %{
|
||||
"country" => object["address"]["addressCountry"],
|
||||
"postal_code" => object["address"]["postalCode"],
|
||||
"region" => object["address"]["addressRegion"],
|
||||
"street" => object["address"]["streetAddress"],
|
||||
"locality" => object["address"]["addressLocality"]
|
||||
})
|
||||
end
|
||||
|
||||
if is_nil(object["geo"]) do
|
||||
res
|
||||
else
|
||||
geo = %Geo.Point{
|
||||
coordinates: {object["geo"]["latitude"], object["geo"]["longitude"]},
|
||||
srid: 4326
|
||||
}
|
||||
|
||||
Map.put(res, "geom", geo)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert an event struct to an ActivityStream representation
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(AddressModel.t()) :: map()
|
||||
def model_to_as(%AddressModel{} = _address) do
|
||||
nil
|
||||
end
|
||||
end
|
@ -12,19 +12,28 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||
alias Mobilizon.Service.ActivityPub.Converter
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.Tag
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map()) :: map()
|
||||
def as_to_model_data(object) do
|
||||
with {:ok, %Actor{id: actor_id}} <- Actors.get_actor_by_url(object["actor"]),
|
||||
tags <- fetch_tags(object["tag"]) do
|
||||
Logger.debug("event as_to_model_data")
|
||||
|
||||
with {:actor, {:ok, %Actor{id: actor_id}}} <-
|
||||
{:actor, Actors.get_actor_by_url(object["actor"])},
|
||||
{:address, address_id} <-
|
||||
{:address, get_address(object["location"])},
|
||||
{:tags, tags} <- {:tags, fetch_tags(object["tag"])} do
|
||||
picture_id =
|
||||
with true <- Map.has_key?(object, "attachment"),
|
||||
with true <- Map.has_key?(object, "attachment") && length(object["attachment"]) > 0,
|
||||
%Picture{id: picture_id} <-
|
||||
Media.get_picture_by_url(
|
||||
object["attachment"]
|
||||
@ -38,27 +47,64 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
%{
|
||||
"title" => object["name"],
|
||||
"description" => object["content"],
|
||||
"organizer_actor_id" => actor_id,
|
||||
"picture_id" => picture_id,
|
||||
"begins_on" => object["begins_on"],
|
||||
"category" => object["category"],
|
||||
"url" => object["id"],
|
||||
"uuid" => object["uuid"],
|
||||
"tags" => tags
|
||||
}
|
||||
{:ok,
|
||||
%{
|
||||
"title" => object["name"],
|
||||
"description" => object["content"],
|
||||
"organizer_actor_id" => actor_id,
|
||||
"picture_id" => picture_id,
|
||||
"begins_on" => object["startTime"],
|
||||
"category" => object["category"],
|
||||
"url" => object["id"],
|
||||
"uuid" => object["uuid"],
|
||||
"tags" => tags,
|
||||
"physical_address_id" => address_id
|
||||
}}
|
||||
else
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_address(%{"id" => url} = map) when is_map(map) and is_binary(url) do
|
||||
Logger.debug("Address with an URL, let's check against our own database")
|
||||
|
||||
case Addresses.get_address_by_url(url) do
|
||||
%Address{id: address_id} ->
|
||||
address_id
|
||||
|
||||
_ ->
|
||||
Logger.debug("not in our database, let's try to create it")
|
||||
map = Map.put(map, "url", map["id"])
|
||||
do_get_address(map)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_address(map) when is_map(map) do
|
||||
do_get_address(map)
|
||||
end
|
||||
|
||||
defp get_address(nil), do: nil
|
||||
|
||||
defp do_get_address(map) do
|
||||
map = Mobilizon.Service.ActivityPub.Converters.Address.as_to_model_data(map)
|
||||
|
||||
case Addresses.create_address(map) do
|
||||
{:ok, %Address{id: address_id}} ->
|
||||
address_id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_tags(tags) do
|
||||
Enum.reduce(tags, [], fn tag, acc ->
|
||||
case Events.get_or_create_tag(tag) do
|
||||
{:ok, %Tag{} = tag} ->
|
||||
acc ++ [tag]
|
||||
|
||||
_ ->
|
||||
with true <- tag["type"] == "Hashtag",
|
||||
{:ok, %Tag{} = tag} <- Events.get_or_create_tag(tag) do
|
||||
acc ++ [tag]
|
||||
else
|
||||
_err ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
@ -132,9 +132,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: validate those with a Ecto scheme
|
||||
# - tags
|
||||
# - emoji
|
||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
|
||||
Logger.info("Handle incoming to create notes")
|
||||
|
||||
@ -159,15 +156,39 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object} = data) do
|
||||
Logger.info("Handle incoming to create event")
|
||||
|
||||
with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(data["actor"]) do
|
||||
Logger.debug("found actor")
|
||||
Logger.debug(inspect(actor))
|
||||
|
||||
params = %{
|
||||
to: data["to"],
|
||||
object: object |> fix_object,
|
||||
actor: actor,
|
||||
local: false,
|
||||
published: data["published"],
|
||||
additional:
|
||||
Map.take(data, [
|
||||
"cc",
|
||||
"id"
|
||||
])
|
||||
}
|
||||
|
||||
ActivityPub.create(params)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
|
||||
) do
|
||||
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true),
|
||||
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
|
||||
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true})
|
||||
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.error("Unable to handle Follow activity")
|
||||
@ -257,9 +278,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
) do
|
||||
with {:ok, %Actor{domain: nil} = followed} <- Actors.get_actor_by_url(followed),
|
||||
{:ok, %Actor{} = follower} <- Actors.get_actor_by_url(follower),
|
||||
{:ok, activity} <- ActivityPub.unfollow(followed, follower, id, false) do
|
||||
{:ok, activity, object} <- ActivityPub.unfollow(followed, follower, id, false) do
|
||||
Actor.unfollow(follower, followed)
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.error(inspect(e))
|
||||
@ -282,11 +303,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||
# TODO : Validate that DELETE comes indeed form right domain (see above)
|
||||
# :ok <- contain_origin(actor_url, object.data),
|
||||
{:ok, activity} <- ActivityPub.delete(object, false) do
|
||||
{:ok, activity}
|
||||
{:ok, activity, object} <- ActivityPub.delete(object, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
Logger.error(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
@ -11,6 +11,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Repo
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event
|
||||
@ -122,7 +123,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
"""
|
||||
def insert_full_object(%{"object" => %{"type" => "Event"} = object_data})
|
||||
when is_map(object_data) do
|
||||
with object_data <-
|
||||
with {:ok, object_data} <-
|
||||
Converters.Event.as_to_model_data(object_data),
|
||||
{:ok, %Event{} = event} <- Events.create_event(object_data) do
|
||||
{:ok, event}
|
||||
@ -260,26 +261,21 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
"""
|
||||
@spec make_event_data(
|
||||
String.t(),
|
||||
String.t(),
|
||||
map(),
|
||||
String.t(),
|
||||
String.t(),
|
||||
map(),
|
||||
list(),
|
||||
list(),
|
||||
map(),
|
||||
String.t()
|
||||
map()
|
||||
) :: map()
|
||||
def make_event_data(
|
||||
actor,
|
||||
to,
|
||||
%{to: to, cc: cc} = _audience,
|
||||
title,
|
||||
content_html,
|
||||
picture \\ nil,
|
||||
tags \\ [],
|
||||
# _cw \\ nil,
|
||||
cc \\ [],
|
||||
metadata \\ %{},
|
||||
category \\ ""
|
||||
metadata \\ %{}
|
||||
) do
|
||||
Logger.debug("Making event data")
|
||||
uuid = Ecto.UUID.generate()
|
||||
@ -287,21 +283,58 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
res = %{
|
||||
"type" => "Event",
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"cc" => cc || [],
|
||||
"content" => content_html,
|
||||
"name" => title,
|
||||
# "summary" => cw,
|
||||
"begins_on" => metadata.begins_on,
|
||||
"category" => category,
|
||||
"startTime" => metadata.begins_on,
|
||||
"category" => metadata.category,
|
||||
"actor" => actor,
|
||||
"id" => Routes.page_url(Endpoint, :event, uuid),
|
||||
"uuid" => uuid,
|
||||
"tag" => tags |> Enum.uniq()
|
||||
"tag" =>
|
||||
tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end)
|
||||
}
|
||||
|
||||
res =
|
||||
if is_nil(metadata.physical_address),
|
||||
do: res,
|
||||
else: Map.put(res, "location", make_address_data(metadata.physical_address))
|
||||
|
||||
if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
|
||||
end
|
||||
|
||||
def make_address_data(%Address{} = address) do
|
||||
res = %{
|
||||
"type" => "Place",
|
||||
"name" => address.description,
|
||||
"id" => address.url,
|
||||
"address" => %{
|
||||
"type" => "PostalAddress",
|
||||
"streetAddress" => address.street,
|
||||
"postalCode" => address.postal_code,
|
||||
"addressLocality" => address.locality,
|
||||
"addressRegion" => address.region,
|
||||
"addressCountry" => address.country
|
||||
}
|
||||
}
|
||||
|
||||
if is_nil(address.geom) do
|
||||
res
|
||||
else
|
||||
Map.put(res, "geo", %{
|
||||
"type" => "GeoCoordinates",
|
||||
"latitude" => address.geom.coordinates |> elem(0),
|
||||
"longitude" => address.geom.coordinates |> elem(1)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def make_address_data(address) do
|
||||
Address
|
||||
|> struct(address)
|
||||
|> make_address_data()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an AP comment object from an set of values
|
||||
"""
|
||||
|
@ -52,7 +52,7 @@ defmodule Mobilizon.Service.Federator do
|
||||
Logger.debug(inspect(params))
|
||||
|
||||
case Transmogrifier.handle_incoming(params) do
|
||||
{:ok, activity} ->
|
||||
{:ok, activity, _} ->
|
||||
{:ok, activity}
|
||||
|
||||
%Activity{} ->
|
||||
|
@ -0,0 +1,9 @@
|
||||
defmodule :"Elixir.Mobilizon.Repo.Migrations.Add-url-to-addresses" do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:addresses) do
|
||||
add(:url, :string, null: false)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,5 +1,5 @@
|
||||
# source: http://localhost:4000/api
|
||||
# timestamp: Fri Jul 26 2019 11:28:32 GMT+0200 (GMT+02:00)
|
||||
# timestamp: Mon Jul 29 2019 15:24:10 GMT+0200 (GMT+02:00)
|
||||
|
||||
schema {
|
||||
query: RootQueryType
|
||||
@ -121,6 +121,25 @@ type Address {
|
||||
street: String
|
||||
}
|
||||
|
||||
input AddressInput {
|
||||
country: String
|
||||
description: String
|
||||
|
||||
"""The floor this event is at"""
|
||||
floor: String
|
||||
|
||||
"""The geocoordinates for the point where this address is"""
|
||||
geom: Point
|
||||
|
||||
"""The address's locality"""
|
||||
locality: String
|
||||
postalCode: String
|
||||
region: String
|
||||
|
||||
"""The address's street name (with number)"""
|
||||
street: String
|
||||
}
|
||||
|
||||
"""A comment"""
|
||||
type Comment {
|
||||
"""Internal ID for this comment"""
|
||||
@ -262,7 +281,7 @@ type Event {
|
||||
"""The Event UUID"""
|
||||
uuid: UUID
|
||||
|
||||
"""The event's visibility"""
|
||||
"""The event's visibility"""
|
||||
visibility: EventVisibility
|
||||
}
|
||||
|
||||
@ -675,6 +694,7 @@ type RootMutationType {
|
||||
onlineAddress: String
|
||||
organizerActorId: ID!
|
||||
phoneAddress: String
|
||||
physicalAddress: AddressInput
|
||||
|
||||
"""
|
||||
The picture for the event, either as an object or directly the ID of an existing Picture
|
||||
@ -891,7 +911,7 @@ type RootQueryType {
|
||||
reverseGeocode(latitude: Float!, longitude: Float!): [Address]
|
||||