Merge branch 'feature/tags' into 'master'
Refactor adding tags to an event See merge request framasoft/mobilizon!162
This commit is contained in:
commit
bcfc26ee59
|
@ -94,6 +94,18 @@ config :geolix,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
config :auto_linker,
|
||||||
|
opts: [
|
||||||
|
scheme: true,
|
||||||
|
extra: true,
|
||||||
|
# TODO: Set to :no_scheme when it works properly
|
||||||
|
validate_tld: true,
|
||||||
|
class: false,
|
||||||
|
strip_prefix: false,
|
||||||
|
new_window: false,
|
||||||
|
rel: false
|
||||||
|
]
|
||||||
|
|
||||||
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
|
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
|
||||||
config :phoenix, :json_library, Jason
|
config :phoenix, :json_library, Jason
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ export default class App extends Vue {
|
||||||
@import "~bulma/sass/components/modal.sass";
|
@import "~bulma/sass/components/modal.sass";
|
||||||
@import "~bulma/sass/components/navbar.sass";
|
@import "~bulma/sass/components/navbar.sass";
|
||||||
@import "~bulma/sass/components/pagination.sass";
|
@import "~bulma/sass/components/pagination.sass";
|
||||||
|
@import "~bulma/sass/components/dropdown.sass";
|
||||||
@import "~bulma/sass/elements/box.sass";
|
@import "~bulma/sass/elements/box.sass";
|
||||||
@import "~bulma/sass/elements/button.sass";
|
@import "~bulma/sass/elements/button.sass";
|
||||||
@import "~bulma/sass/elements/container.sass";
|
@import "~bulma/sass/elements/container.sass";
|
||||||
|
@ -91,9 +92,11 @@ export default class App extends Vue {
|
||||||
@import "~buefy/src/scss/components/datepicker";
|
@import "~buefy/src/scss/components/datepicker";
|
||||||
@import "~buefy/src/scss/components/notices";
|
@import "~buefy/src/scss/components/notices";
|
||||||
@import "~buefy/src/scss/components/dropdown";
|
@import "~buefy/src/scss/components/dropdown";
|
||||||
|
@import "~buefy/src/scss/components/autocomplete";
|
||||||
@import "~buefy/src/scss/components/form";
|
@import "~buefy/src/scss/components/form";
|
||||||
@import "~buefy/src/scss/components/modal";
|
@import "~buefy/src/scss/components/modal";
|
||||||
@import "~buefy/src/scss/components/tag";
|
@import "~buefy/src/scss/components/tag";
|
||||||
|
@import "~buefy/src/scss/components/taginput";
|
||||||
@import "~buefy/src/scss/components/upload";
|
@import "~buefy/src/scss/components/upload";
|
||||||
|
|
||||||
.router-enter-active,
|
.router-enter-active,
|
||||||
|
|
54
js/src/components/Event/TagInput.vue
Normal file
54
js/src/components/Event/TagInput.vue
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<b-field label="Enter some tags">
|
||||||
|
<b-taginput
|
||||||
|
v-model="tags"
|
||||||
|
:data="filteredTags"
|
||||||
|
autocomplete
|
||||||
|
:allow-new="true"
|
||||||
|
:field="path"
|
||||||
|
icon="label"
|
||||||
|
placeholder="Add a tag"
|
||||||
|
@typing="getFilteredTags">
|
||||||
|
</b-taginput>
|
||||||
|
</b-field>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { ITag } from '@/types/tag.model';
|
||||||
|
@Component
|
||||||
|
export default class TagInput extends Vue {
|
||||||
|
|
||||||
|
@Prop({ required: false, default: () => [] }) data!: object[];
|
||||||
|
@Prop({ required: true, default: 'value' }) path!: string;
|
||||||
|
@Prop({ required: true }) value!: string;
|
||||||
|
|
||||||
|
filteredTags: object[] = [];
|
||||||
|
tags: object[] = [];
|
||||||
|
|
||||||
|
getFilteredTags(text) {
|
||||||
|
this.filteredTags = this.data.filter((option) => {
|
||||||
|
return get(option, this.path)
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(text.toLowerCase()) >= 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('tags')
|
||||||
|
onTagsChanged (tags) {
|
||||||
|
const tagEntities = tags.map((tag) => {
|
||||||
|
if (TagInput.isTag(tag)) {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
return { title: tag, slug: tag } as ITag;
|
||||||
|
});
|
||||||
|
console.log('tags changed', tagEntities);
|
||||||
|
this.$emit('input', tagEntities);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isTag(x: any): x is ITag {
|
||||||
|
return x.slug !== undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -143,7 +143,8 @@ export const CREATE_EVENT = gql`
|
||||||
$organizerActorId: ID!,
|
$organizerActorId: ID!,
|
||||||
$category: String!,
|
$category: String!,
|
||||||
$beginsOn: DateTime!,
|
$beginsOn: DateTime!,
|
||||||
$picture: PictureInput!
|
$picture: PictureInput,
|
||||||
|
$tags: [String]
|
||||||
) {
|
) {
|
||||||
createEvent(
|
createEvent(
|
||||||
title: $title,
|
title: $title,
|
||||||
|
@ -151,7 +152,8 @@ export const CREATE_EVENT = gql`
|
||||||
beginsOn: $beginsOn,
|
beginsOn: $beginsOn,
|
||||||
organizerActorId: $organizerActorId,
|
organizerActorId: $organizerActorId,
|
||||||
category: $category,
|
category: $category,
|
||||||
picture: $picture
|
picture: $picture,
|
||||||
|
tags: $tags
|
||||||
) {
|
) {
|
||||||
id,
|
id,
|
||||||
uuid,
|
uuid,
|
||||||
|
@ -203,8 +205,10 @@ export const LEAVE_EVENT = gql`
|
||||||
export const DELETE_EVENT = gql`
|
export const DELETE_EVENT = gql`
|
||||||
mutation DeleteEvent($id: Int!, $actorId: Int!) {
|
mutation DeleteEvent($id: Int!, $actorId: Int!) {
|
||||||
deleteEvent(
|
deleteEvent(
|
||||||
id: $id,
|
eventId: $id,
|
||||||
actorId: $actorId
|
actorId: $actorId
|
||||||
)
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
16
js/src/graphql/tags.ts
Normal file
16
js/src/graphql/tags.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
export const TAGS = gql`
|
||||||
|
query {
|
||||||
|
tags {
|
||||||
|
id,
|
||||||
|
related {
|
||||||
|
id,
|
||||||
|
slug,
|
||||||
|
title
|
||||||
|
}
|
||||||
|
slug,
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
|
@ -75,6 +75,8 @@ export interface IEvent {
|
||||||
onlineAddress?: string;
|
onlineAddress?: string;
|
||||||
phoneAddress?: string;
|
phoneAddress?: string;
|
||||||
physicalAddress?: IAddress;
|
physicalAddress?: IAddress;
|
||||||
|
|
||||||
|
tags: ITag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,4 +101,5 @@ export class EventModel implements IEvent {
|
||||||
onlineAddress: string = '';
|
onlineAddress: string = '';
|
||||||
phoneAddress: string = '';
|
phoneAddress: string = '';
|
||||||
picture: IAbstractPicture|null = null;
|
picture: IAbstractPicture|null = null;
|
||||||
|
tags: ITag[] = [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,14 @@
|
||||||
<div v-if="$apollo.loading">Loading...</div>
|
<div v-if="$apollo.loading">Loading...</div>
|
||||||
<div class="columns is-centered" v-else>
|
<div class="columns is-centered" v-else>
|
||||||
<form class="column is-two-thirds-desktop" @submit="createEvent">
|
<form class="column is-two-thirds-desktop" @submit="createEvent">
|
||||||
|
<picture-upload v-model="pictureFile" />
|
||||||
|
|
||||||
<b-field :label="$gettext('Title')">
|
<b-field :label="$gettext('Title')">
|
||||||
<b-input aria-required="true" required v-model="event.title" maxlength="64" />
|
<b-input aria-required="true" required v-model="event.title" maxlength="64" />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
<tag-input v-model="event.tags" :data="tags" path="title" />
|
||||||
|
|
||||||
<date-time-picker v-model="event.beginsOn" :label="$gettext('Starts on…')" :step="15"/>
|
<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" />
|
<date-time-picker v-model="event.endsOn" :label="$gettext('Ends on…')" :step="15" />
|
||||||
|
|
||||||
|
@ -28,8 +32,6 @@
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<picture-upload v-model="pictureFile" />
|
|
||||||
|
|
||||||
<button class="button is-primary">
|
<button class="button is-primary">
|
||||||
<translate>Create my event</translate>
|
<translate>Create my event</translate>
|
||||||
</button>
|
</button>
|
||||||
|
@ -52,13 +54,19 @@ import { IPerson, Person } from '@/types/actor';
|
||||||
import PictureUpload from '@/components/PictureUpload.vue';
|
import PictureUpload from '@/components/PictureUpload.vue';
|
||||||
import Editor from '@/components/Editor.vue';
|
import Editor from '@/components/Editor.vue';
|
||||||
import DateTimePicker from '@/components/Event/DateTimePicker.vue';
|
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';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { DateTimePicker, PictureUpload, Editor },
|
components: { TagInput, DateTimePicker, PictureUpload, Editor },
|
||||||
apollo: {
|
apollo: {
|
||||||
loggedPerson: {
|
loggedPerson: {
|
||||||
query: LOGGED_PERSON,
|
query: LOGGED_PERSON,
|
||||||
},
|
},
|
||||||
|
tags: {
|
||||||
|
query: TAGS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class CreateEvent extends Vue {
|
export default class CreateEvent extends Vue {
|
||||||
|
@ -123,11 +131,12 @@ export default class CreateEvent extends Vue {
|
||||||
* Transform general variables
|
* Transform general variables
|
||||||
*/
|
*/
|
||||||
let pictureObj = {};
|
let pictureObj = {};
|
||||||
let obj = {
|
const obj = {
|
||||||
organizerActorId: this.loggedPerson.id,
|
organizerActorId: this.loggedPerson.id,
|
||||||
beginsOn: this.event.beginsOn.toISOString(),
|
beginsOn: this.event.beginsOn.toISOString(),
|
||||||
|
tags: this.event.tags.map((tag: ITag) => tag.title),
|
||||||
};
|
};
|
||||||
let res = Object.assign({}, this.event, obj);
|
const res = Object.assign({}, this.event, obj);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform picture files
|
* Transform picture files
|
||||||
|
|
|
@ -86,7 +86,6 @@ defmodule Mobilizon.Events.Event do
|
||||||
:uuid,
|
:uuid,
|
||||||
:picture_id
|
:picture_id
|
||||||
])
|
])
|
||||||
|> cast_assoc(:tags)
|
|
||||||
|> cast_assoc(:physical_address)
|
|> cast_assoc(:physical_address)
|
||||||
|> validate_required([
|
|> validate_required([
|
||||||
:title,
|
:title,
|
||||||
|
|
|
@ -367,7 +367,7 @@ defmodule Mobilizon.Events do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def create_event(attrs \\ %{}) do
|
def create_event(attrs \\ %{}) do
|
||||||
with {:ok, %Event{} = event} <- %Event{} |> Event.changeset(attrs) |> Repo.insert(),
|
with %Event{} = event <- do_create_event(attrs),
|
||||||
{:ok, %Participant{} = _participant} <-
|
{:ok, %Participant{} = _participant} <-
|
||||||
%Participant{}
|
%Participant{}
|
||||||
|> Participant.changeset(%{
|
|> Participant.changeset(%{
|
||||||
|
@ -376,7 +376,24 @@ defmodule Mobilizon.Events do
|
||||||
event_id: event.id
|
event_id: event.id
|
||||||
})
|
})
|
||||||
|> Repo.insert() do
|
|> Repo.insert() do
|
||||||
{:ok, Repo.preload(event, [:organizer_actor])}
|
{:ok, event}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_create_event(attrs) do
|
||||||
|
with {:ok, %Event{} = event} <- %Event{} |> Event.changeset(attrs) |> Repo.insert(),
|
||||||
|
%Event{} = event <- event |> Repo.preload([:tags, :organizer_actor]),
|
||||||
|
{:has_tags, true, _} <- {:has_tags, Map.has_key?(attrs, "tags"), event} do
|
||||||
|
event
|
||||||
|
|> Ecto.Changeset.change()
|
||||||
|
|> Ecto.Changeset.put_assoc(:tags, attrs["tags"])
|
||||||
|
|> Repo.update()
|
||||||
|
else
|
||||||
|
{:has_tags, false, event} ->
|
||||||
|
event
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -491,6 +508,22 @@ defmodule Mobilizon.Events do
|
||||||
"""
|
"""
|
||||||
def get_tag!(id), do: Repo.get!(Tag, id)
|
def get_tag!(id), do: Repo.get!(Tag, id)
|
||||||
|
|
||||||
|
def get_tag(id), do: Repo.get(Tag, id)
|
||||||
|
|
||||||
|
@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
|
||||||
|
case Repo.get_by(Tag, title: title) do
|
||||||
|
%Tag{} = tag ->
|
||||||
|
{:ok, tag}
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
create_tag(%{"title" => title})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a tag.
|
Creates a tag.
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,9 @@ defmodule MobilizonWeb.API.Comments do
|
||||||
alias Mobilizon.Actors
|
alias Mobilizon.Actors
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Events.Comment
|
alias Mobilizon.Events.Comment
|
||||||
alias Mobilizon.Service.Formatter
|
|
||||||
alias Mobilizon.Service.ActivityPub
|
alias Mobilizon.Service.ActivityPub
|
||||||
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
||||||
import MobilizonWeb.API.Utils
|
alias MobilizonWeb.API.Utils
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Create a comment
|
Create a comment
|
||||||
|
@ -20,23 +19,14 @@ defmodule MobilizonWeb.API.Comments do
|
||||||
def create_comment(
|
def create_comment(
|
||||||
from_username,
|
from_username,
|
||||||
status,
|
status,
|
||||||
visibility \\ "public",
|
visibility \\ :public,
|
||||||
in_reply_to_comment_URL \\ nil
|
in_reply_to_comment_URL \\ nil
|
||||||
) do
|
) do
|
||||||
with {:local_actor, %Actor{url: url} = actor} <-
|
with {:local_actor, %Actor{url: url} = actor} <-
|
||||||
{:local_actor, Actors.get_local_actor_by_name(from_username)},
|
{:local_actor, Actors.get_local_actor_by_name(from_username)},
|
||||||
status <- String.trim(status),
|
|
||||||
mentions <- Formatter.parse_mentions(status),
|
|
||||||
in_reply_to_comment <- get_in_reply_to_comment(in_reply_to_comment_URL),
|
in_reply_to_comment <- get_in_reply_to_comment(in_reply_to_comment_URL),
|
||||||
{to, cc} <- to_for_actor_and_mentions(actor, mentions, in_reply_to_comment, visibility),
|
{content_html, tags, to, cc} <-
|
||||||
tags <- Formatter.parse_tags(status),
|
Utils.prepare_content(actor, status, visibility, [], in_reply_to_comment),
|
||||||
content_html <-
|
|
||||||
make_content_html(
|
|
||||||
status,
|
|
||||||
mentions,
|
|
||||||
tags,
|
|
||||||
"text/plain"
|
|
||||||
),
|
|
||||||
comment <-
|
comment <-
|
||||||
ActivityPubUtils.make_comment_data(
|
ActivityPubUtils.make_comment_data(
|
||||||
url,
|
url,
|
||||||
|
|
|
@ -4,10 +4,9 @@ defmodule MobilizonWeb.API.Events do
|
||||||
"""
|
"""
|
||||||
alias Mobilizon.Actors
|
alias Mobilizon.Actors
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Service.Formatter
|
|
||||||
alias Mobilizon.Service.ActivityPub
|
alias Mobilizon.Service.ActivityPub
|
||||||
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
||||||
import MobilizonWeb.API.Utils
|
alias MobilizonWeb.API.Utils
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Create an event
|
Create an event
|
||||||
|
@ -19,24 +18,19 @@ defmodule MobilizonWeb.API.Events do
|
||||||
description: description,
|
description: description,
|
||||||
organizer_actor_id: organizer_actor_id,
|
organizer_actor_id: organizer_actor_id,
|
||||||
begins_on: begins_on,
|
begins_on: begins_on,
|
||||||
category: category
|
category: category,
|
||||||
|
tags: tags
|
||||||
} = args
|
} = args
|
||||||
) do
|
) do
|
||||||
|
require Logger
|
||||||
|
|
||||||
with %Actor{url: url} = actor <-
|
with %Actor{url: url} = actor <-
|
||||||
Actors.get_local_actor_with_everything(organizer_actor_id),
|
Actors.get_local_actor_with_everything(organizer_actor_id),
|
||||||
title <- String.trim(title),
|
title <- String.trim(title),
|
||||||
mentions <- Formatter.parse_mentions(description),
|
|
||||||
visibility <- Map.get(args, :visibility, :public),
|
visibility <- Map.get(args, :visibility, :public),
|
||||||
{to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, Atom.to_string(visibility)),
|
|
||||||
tags <- Formatter.parse_tags(description),
|
|
||||||
picture <- Map.get(args, :picture, nil),
|
picture <- Map.get(args, :picture, nil),
|
||||||
content_html <-
|
{content_html, tags, to, cc} <-
|
||||||
make_content_html(
|
Utils.prepare_content(actor, description, visibility, tags, nil),
|
||||||
description,
|
|
||||||
mentions,
|
|
||||||
tags,
|
|
||||||
"text/plain"
|
|
||||||
),
|
|
||||||
event <-
|
event <-
|
||||||
ActivityPubUtils.make_event_data(
|
ActivityPubUtils.make_event_data(
|
||||||
url,
|
url,
|
||||||
|
|
|
@ -4,10 +4,9 @@ defmodule MobilizonWeb.API.Groups do
|
||||||
"""
|
"""
|
||||||
alias Mobilizon.Actors
|
alias Mobilizon.Actors
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Service.Formatter
|
|
||||||
alias Mobilizon.Service.ActivityPub
|
alias Mobilizon.Service.ActivityPub
|
||||||
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
||||||
import MobilizonWeb.API.Utils
|
alias MobilizonWeb.API.Utils
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Create a group
|
Create a group
|
||||||
|
@ -24,17 +23,9 @@ defmodule MobilizonWeb.API.Groups do
|
||||||
{:bad_actor, Actors.get_local_actor_by_name(admin_actor_username)},
|
{:bad_actor, Actors.get_local_actor_by_name(admin_actor_username)},
|
||||||
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
|
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
|
||||||
title <- String.trim(title),
|
title <- String.trim(title),
|
||||||
mentions <- Formatter.parse_mentions(description),
|
visibility <- Map.get(args, :visibility, :public),
|
||||||
visibility <- Map.get(args, :visibility, "public"),
|
{content_html, tags, to, cc} <-
|
||||||
{to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, visibility),
|
Utils.prepare_content(actor, description, visibility, [], nil),
|
||||||
tags <- Formatter.parse_tags(description),
|
|
||||||
content_html <-
|
|
||||||
make_content_html(
|
|
||||||
description,
|
|
||||||
mentions,
|
|
||||||
tags,
|
|
||||||
"text/plain"
|
|
||||||
),
|
|
||||||
group <-
|
group <-
|
||||||
ActivityPubUtils.make_group_data(
|
ActivityPubUtils.make_group_data(
|
||||||
url,
|
url,
|
||||||
|
|
|
@ -12,11 +12,9 @@ defmodule MobilizonWeb.API.Utils do
|
||||||
* `to` : the mentionned actors, the eventual actor we're replying to and the public
|
* `to` : the mentionned actors, the eventual actor we're replying to and the public
|
||||||
* `cc` : the actor's followers
|
* `cc` : the actor's followers
|
||||||
"""
|
"""
|
||||||
@spec to_for_actor_and_mentions(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
||||||
def to_for_actor_and_mentions(%Actor{} = actor, mentions, inReplyTo, "public") do
|
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :public) do
|
||||||
mentioned_actors = Enum.map(mentions, fn {_, %{url: url}} -> url end)
|
to = ["https://www.w3.org/ns/activitystreams#Public" | mentions]
|
||||||
|
|
||||||
to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_actors]
|
|
||||||
cc = [actor.followers_url]
|
cc = [actor.followers_url]
|
||||||
|
|
||||||
if inReplyTo do
|
if inReplyTo do
|
||||||
|
@ -33,11 +31,9 @@ defmodule MobilizonWeb.API.Utils do
|
||||||
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
|
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
|
||||||
* `cc` : public
|
* `cc` : public
|
||||||
"""
|
"""
|
||||||
@spec to_for_actor_and_mentions(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
||||||
def to_for_actor_and_mentions(%Actor{} = actor, mentions, inReplyTo, "unlisted") do
|
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :unlisted) do
|
||||||
mentioned_actors = Enum.map(mentions, fn {_, %{url: url}} -> url end)
|
to = [actor.followers_url | mentions]
|
||||||
|
|
||||||
to = [actor.followers_url | mentioned_actors]
|
|
||||||
cc = ["https://www.w3.org/ns/activitystreams#Public"]
|
cc = ["https://www.w3.org/ns/activitystreams#Public"]
|
||||||
|
|
||||||
if inReplyTo do
|
if inReplyTo do
|
||||||
|
@ -54,9 +50,9 @@ defmodule MobilizonWeb.API.Utils do
|
||||||
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
|
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
|
||||||
* `cc` : none
|
* `cc` : none
|
||||||
"""
|
"""
|
||||||
@spec to_for_actor_and_mentions(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
||||||
def to_for_actor_and_mentions(%Actor{} = actor, mentions, inReplyTo, "private") do
|
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :private) do
|
||||||
{to, cc} = to_for_actor_and_mentions(actor, mentions, inReplyTo, "direct")
|
{to, cc} = get_to_and_cc(actor, mentions, inReplyTo, :direct)
|
||||||
{[actor.followers_url | to], cc}
|
{[actor.followers_url | to], cc}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,59 +63,62 @@ defmodule MobilizonWeb.API.Utils do
|
||||||
* `to` : the mentionned actors and the eventual actor we're replying to
|
* `to` : the mentionned actors and the eventual actor we're replying to
|
||||||
* `cc` : none
|
* `cc` : none
|
||||||
"""
|
"""
|
||||||
@spec to_for_actor_and_mentions(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
||||||
def to_for_actor_and_mentions(_actor, mentions, inReplyTo, "direct") do
|
def get_to_and_cc(_actor, mentions, inReplyTo, :direct) do
|
||||||
mentioned_actors = Enum.map(mentions, fn {_, %{url: url}} -> url end)
|
|
||||||
|
|
||||||
if inReplyTo do
|
if inReplyTo do
|
||||||
{Enum.uniq([inReplyTo.actor | mentioned_actors]), []}
|
{Enum.uniq([inReplyTo.actor | mentions]), []}
|
||||||
else
|
else
|
||||||
{mentioned_actors, []}
|
{mentions, []}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
|
||||||
|
|
||||||
|
# def get_addressed_users(_, to) when is_list(to) do
|
||||||
|
# Actors.get(to)
|
||||||
|
# end
|
||||||
|
|
||||||
|
def get_addressed_users(mentioned_users, _), do: mentioned_users
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates HTML content from text and mentions
|
Creates HTML content from text and mentions
|
||||||
"""
|
"""
|
||||||
@spec make_content_html(String.t(), list(), list(), String.t()) :: String.t()
|
@spec make_content_html(String.t(), list(), String.t()) :: String.t()
|
||||||
def make_content_html(
|
def make_content_html(
|
||||||
status,
|
text,
|
||||||
mentions,
|
additional_tags,
|
||||||
tags,
|
|
||||||
content_type
|
content_type
|
||||||
),
|
) do
|
||||||
do: format_input(status, mentions, tags, content_type)
|
with {text, mentions, tags} <- format_input(text, content_type, []) do
|
||||||
|
{text, mentions, additional_tags ++ Enum.map(tags, fn {_, tag} -> tag end)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def format_input(text, mentions, tags, "text/plain") do
|
def format_input(text, "text/plain", options) do
|
||||||
text
|
text
|
||||||
|> Formatter.html_escape("text/plain")
|
|> Formatter.html_escape("text/plain")
|
||||||
|> String.replace(~r/\r?\n/, "<br>")
|
|> Formatter.linkify(options)
|
||||||
|> (&{[], &1}).()
|
|> (fn {text, mentions, tags} ->
|
||||||
|> Formatter.add_links()
|
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
|
||||||
|> Formatter.add_actor_links(mentions)
|
end).()
|
||||||
|> Formatter.add_hashtag_links(tags)
|
|
||||||
|> Formatter.finalize()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_input(text, mentions, _tags, "text/html") do
|
def format_input(text, "text/html", options) do
|
||||||
text
|
text
|
||||||
|> Formatter.html_escape("text/html")
|
|> Formatter.html_escape("text/html")
|
||||||
|> String.replace(~r/\r?\n/, "<br>")
|
|> Formatter.linkify(options)
|
||||||
|> (&{[], &1}).()
|
|
||||||
|> Formatter.add_actor_links(mentions)
|
|
||||||
|> Formatter.finalize()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# def format_input(text, mentions, tags, "text/markdown") do
|
# @doc """
|
||||||
# text
|
# Formatting text to markdown.
|
||||||
# |> Earmark.as_html!()
|
# """
|
||||||
# |> Formatter.html_escape("text/html")
|
# def format_input(text, "text/markdown", options) do
|
||||||
# |> String.replace(~r/\r?\n/, "")
|
# text
|
||||||
# |> (&{[], &1}).()
|
# |> Formatter.mentions_escape(options)
|
||||||
# |> Formatter.add_actor_links(mentions)
|
# |> Earmark.as_html!()
|
||||||
# |> Formatter.add_hashtag_links(tags)
|
# |> Formatter.linkify(options)
|
||||||
# |> Formatter.finalize()
|
# |> Formatter.html_escape("text/html")
|
||||||
# end
|
# end
|
||||||
|
|
||||||
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
|
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
|
||||||
|
|
||||||
|
@ -132,4 +131,19 @@ defmodule MobilizonWeb.API.Utils do
|
||||||
{:error, "Comment must be up to #{max_size} characters"}
|
{:error, "Comment must be up to #{max_size} characters"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prepare_content(actor, content, visibility, tags, in_reply_to) do
|
||||||
|
with content <- String.trim(content),
|
||||||
|
{content_html, mentions, tags} <-
|
||||||
|
make_content_html(
|
||||||
|
content,
|
||||||
|
tags,
|
||||||
|
"text/plain"
|
||||||
|
),
|
||||||
|
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.url),
|
||||||
|
addressed_users <- get_addressed_users(mentioned_users, nil),
|
||||||
|
{to, cc} <- get_to_and_cc(actor, addressed_users, in_reply_to, visibility) do
|
||||||
|
{content_html, tags, to, cc}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@ defmodule MobilizonWeb.Resolvers.Tag do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Handles the tag-related GraphQL calls
|
Handles the tag-related GraphQL calls
|
||||||
"""
|
"""
|
||||||
require Logger
|
alias Mobilizon.Events
|
||||||
alias Mobilizon.Events.Event
|
alias Mobilizon.Events.Event
|
||||||
alias Mobilizon.Events.Tag
|
alias Mobilizon.Events.Tag
|
||||||
|
|
||||||
|
@ -19,6 +19,15 @@ defmodule MobilizonWeb.Resolvers.Tag do
|
||||||
{:ok, Mobilizon.Events.list_tags_for_event(id)}
|
{:ok, Mobilizon.Events.list_tags_for_event(id)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieve the list of tags for an event
|
||||||
|
"""
|
||||||
|
def list_tags_for_event(%{url: url}, _args, _resolution) do
|
||||||
|
with %Event{id: event_id} <- Events.get_event_by_url(url) do
|
||||||
|
{:ok, Mobilizon.Events.list_tags_for_event(event_id)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# @doc """
|
# @doc """
|
||||||
# Retrieve the list of related tags for a given tag ID
|
# Retrieve the list of related tags for a given tag ID
|
||||||
# """
|
# """
|
||||||
|
|
|
@ -117,6 +117,11 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||||
arg(:public, :boolean)
|
arg(:public, :boolean)
|
||||||
arg(:visibility, :event_visibility, default_value: :private)
|
arg(:visibility, :event_visibility, default_value: :private)
|
||||||
|
|
||||||
|
arg(:tags, list_of(:string),
|
||||||
|
default_value: [],
|
||||||
|
description: "The list of tags associated to the event"
|
||||||
|
)
|
||||||
|
|
||||||
arg(:picture, :picture_input,
|
arg(:picture, :picture_input,
|
||||||
description:
|
description:
|
||||||
"The picture for the event, either as an object or directly the ID of an existing Picture"
|
"The picture for the event, either as an object or directly the ID of an existing Picture"
|
||||||
|
|
|
@ -10,6 +10,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Events.Event, as: EventModel
|
alias Mobilizon.Events.Event, as: EventModel
|
||||||
alias Mobilizon.Service.ActivityPub.Converter
|
alias Mobilizon.Service.ActivityPub.Converter
|
||||||
|
alias Mobilizon.Events
|
||||||
|
alias Mobilizon.Events.Tag
|
||||||
|
|
||||||
@behaviour Converter
|
@behaviour Converter
|
||||||
|
|
||||||
|
@ -19,7 +21,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||||
@impl Converter
|
@impl Converter
|
||||||
@spec as_to_model_data(map()) :: map()
|
@spec as_to_model_data(map()) :: map()
|
||||||
def as_to_model_data(object) do
|
def as_to_model_data(object) do
|
||||||
with {:ok, %Actor{id: actor_id}} <- Actors.get_actor_by_url(object["actor"]) do
|
with {:ok, %Actor{id: actor_id}} <- Actors.get_actor_by_url(object["actor"]),
|
||||||
|
tags <- fetch_tags(object["tag"]) do
|
||||||
picture_id =
|
picture_id =
|
||||||
with true <- Map.has_key?(object, "attachment"),
|
with true <- Map.has_key?(object, "attachment"),
|
||||||
%Picture{id: picture_id} <-
|
%Picture{id: picture_id} <-
|
||||||
|
@ -43,11 +46,24 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||||
"begins_on" => object["begins_on"],
|
"begins_on" => object["begins_on"],
|
||||||
"category" => object["category"],
|
"category" => object["category"],
|
||||||
"url" => object["id"],
|
"url" => object["id"],
|
||||||
"uuid" => object["uuid"]
|
"uuid" => object["uuid"],
|
||||||
|
"tags" => tags
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
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]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Convert an event struct to an ActivityStream representation
|
Convert an event struct to an ActivityStream representation
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -296,7 +296,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||||
"actor" => actor,
|
"actor" => actor,
|
||||||
"id" => Routes.page_url(Endpoint, :event, uuid),
|
"id" => Routes.page_url(Endpoint, :event, uuid),
|
||||||
"uuid" => uuid,
|
"uuid" => uuid,
|
||||||
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
|
"tag" => tags |> Enum.uniq()
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
|
if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
|
||||||
|
@ -328,7 +328,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||||
"actor" => actor,
|
"actor" => actor,
|
||||||
"id" => Routes.page_url(Endpoint, :comment, uuid),
|
"id" => Routes.page_url(Endpoint, :comment, uuid),
|
||||||
"uuid" => uuid,
|
"uuid" => uuid,
|
||||||
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
|
"tag" => tags |> Enum.uniq()
|
||||||
}
|
}
|
||||||
|
|
||||||
if inReplyTo do
|
if inReplyTo do
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Portions of this file are derived from Pleroma:
|
# Portions of this file are derived from Pleroma:
|
||||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/formatter.ex
|
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/formatter.ex
|
||||||
|
|
||||||
|
@ -10,68 +10,86 @@ defmodule Mobilizon.Service.Formatter do
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Actors
|
alias Mobilizon.Actors
|
||||||
|
|
||||||
@tag_regex ~r/\#\w+/u
|
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
|
||||||
def parse_tags(text, data \\ %{}) do
|
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
|
||||||
Regex.scan(@tag_regex, text)
|
|
||||||
|> Enum.map(fn ["#" <> tag = full_tag] -> {full_tag, String.downcase(tag)} end)
|
@auto_linker_config hashtag: true,
|
||||||
|> (fn map ->
|
hashtag_handler: &Mobilizon.Service.Formatter.hashtag_handler/4,
|
||||||
if data["sensitive"] in [true, "True", "true", "1"],
|
mention: true,
|
||||||
do: [{"#nsfw", "nsfw"}] ++ map,
|
mention_handler: &Mobilizon.Service.Formatter.mention_handler/4
|
||||||
else: map
|
|
||||||
end).()
|
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
|
||||||
|
case Mobilizon.Actors.get_actor_by_name(nickname) do
|
||||||
|
%Actor{} ->
|
||||||
|
# escape markdown characters with `\\`
|
||||||
|
# (we don't want something like @user__name to be parsed by markdown)
|
||||||
|
String.replace(mention, @markdown_characters_regex, "\\\\\\1")
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
buffer
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_mentions(text) do
|
def mention_handler("@" <> nickname, buffer, _opts, acc) do
|
||||||
# Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
|
case Actors.get_actor_by_name(nickname) do
|
||||||
regex =
|
%Actor{id: id, url: url, preferred_username: preferred_username} = actor ->
|
||||||
~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
|
link =
|
||||||
|
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{url}'>@<span>#{
|
||||||
|
preferred_username
|
||||||
|
}</span></a></span>"
|
||||||
|
|
||||||
Regex.scan(regex, text)
|
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}}
|
||||||
|> List.flatten()
|
|
||||||
|> Enum.uniq()
|
_ ->
|
||||||
|> Enum.map(fn "@" <> match = full_match ->
|
{buffer, acc}
|
||||||
{full_match, Actors.get_actor_by_name(match)}
|
end
|
||||||
end)
|
|
||||||
|> Enum.filter(fn {_match, user} -> user end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# def emojify(text) do
|
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
|
||||||
# emojify(text, Emoji.get_all())
|
tag = String.downcase(tag)
|
||||||
# end
|
url = "#{MobilizonWeb.Endpoint.url()}/tag/#{tag}"
|
||||||
|
link = "<a class='hashtag' data-tag='#{tag}' href='#{url}' rel='tag'>#{tag_text}</a>"
|
||||||
|
|
||||||
# def emojify(text, nil), do: text
|
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
|
||||||
|
end
|
||||||
|
|
||||||
# def emojify(text, emoji) do
|
@doc """
|
||||||
# Enum.reduce(emoji, text, fn {emoji, file}, text ->
|
Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
|
||||||
# emoji = HTML.strip_tags(emoji)
|
|
||||||
# file = HTML.strip_tags(file)
|
|
||||||
|
|
||||||
# String.replace(
|
If the 'safe_mention' option is given, only consecutive mentions at the start the post are actually mentioned.
|
||||||
# text,
|
"""
|
||||||
# ":#{emoji}:",
|
@spec linkify(String.t(), keyword()) ::
|
||||||
# "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
|
{String.t(), [{String.t(), Actor.t()}], [{String.t(), String.t()}]}
|
||||||
# MediaProxy.url(file)
|
def linkify(text, options \\ []) do
|
||||||
# }' />"
|
options = options ++ @auto_linker_config
|
||||||
# )
|
|
||||||
# |> HTML.filter_tags()
|
|
||||||
# end)
|
|
||||||
# end
|
|
||||||
|
|
||||||
# def get_emoji(text) when is_binary(text) do
|
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
||||||
# Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
|
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
|
||||||
# end
|
|
||||||
|
|
||||||
# def get_emoji(_), do: []
|
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
||||||
|
end
|
||||||
|
|
||||||
@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
|
@doc """
|
||||||
|
Escapes a special characters in mention names.
|
||||||
|
"""
|
||||||
|
def mentions_escape(text, options \\ []) do
|
||||||
|
options =
|
||||||
|
Keyword.merge(options,
|
||||||
|
mention: true,
|
||||||
|
url: false,
|
||||||
|
mention_handler: &escape_mention_handler/4
|
||||||
|
)
|
||||||
|
|
||||||
@uri_schemes Application.get_env(:mobilizon, :uri_schemes, [])
|
AutoLinker.link(text, options)
|
||||||
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
|
end
|
||||||
|
|
||||||
# # TODO: make it use something other than @link_regex
|
def html_escape({text, mentions, hashtags}, type) do
|
||||||
# def html_escape(text, "text/html") do
|
{html_escape(text, type), mentions, hashtags}
|
||||||
# HTML.filter_tags(text)
|
end
|
||||||
# end
|
|
||||||
|
def html_escape(_text, "text/html") do
|
||||||
|
# HTML.filter_tags(text)
|
||||||
|
end
|
||||||
|
|
||||||
def html_escape(text, "text/plain") do
|
def html_escape(text, "text/plain") do
|
||||||
Regex.split(@link_regex, text, include_captures: true)
|
Regex.split(@link_regex, text, include_captures: true)
|
||||||
|
@ -82,84 +100,15 @@ defmodule Mobilizon.Service.Formatter do
|
||||||
|> Enum.join("")
|
|> Enum.join("")
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "changes scheme:... urls to html links"
|
def truncate(text, max_length \\ 200, omission \\ "...") do
|
||||||
def add_links({subs, text}) do
|
# Remove trailing whitespace
|
||||||
links =
|
text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
|
||||||
|
|
||||||
|
if String.length(text) < max_length do
|
||||||
text
|
text
|
||||||
|> String.split([" ", "\t", "<br>"])
|
else
|
||||||
|> Enum.filter(fn word -> String.starts_with?(word, @valid_schemes) end)
|
length_with_omission = max_length - String.length(omission)
|
||||||
|> Enum.filter(fn word -> Regex.match?(@link_regex, word) end)
|
String.slice(text, 0, length_with_omission) <> omission
|
||||||
|> Enum.map(fn url -> {Ecto.UUID.generate(), url} end)
|
end
|
||||||
|> Enum.sort_by(fn {_, url} -> -String.length(url) end)
|
|
||||||
|
|
||||||
uuid_text =
|
|
||||||
links
|
|
||||||
|> Enum.reduce(text, fn {uuid, url}, acc -> String.replace(acc, url, uuid) end)
|
|
||||||
|
|
||||||
subs =
|
|
||||||
subs ++
|
|
||||||
Enum.map(links, fn {uuid, url} ->
|
|
||||||
{uuid, "<a href=\"#{url}\">#{url}</a>"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
{subs, uuid_text}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc "Adds the links to mentioned actors"
|
|
||||||
def add_actor_links({subs, text}, mentions) do
|
|
||||||
mentions =
|
|
||||||
mentions
|
|
||||||
|> Enum.sort_by(fn {name, _} -> -String.length(name) end)
|
|
||||||
|> Enum.map(fn {name, actor} -> {name, actor, Ecto.UUID.generate()} end)
|
|
||||||
|
|
||||||
uuid_text =
|
|
||||||
mentions
|
|
||||||
|> Enum.reduce(text, fn {match, _actor, uuid}, text ->
|
|
||||||
String.replace(text, match, uuid)
|
|
||||||
end)
|
|
||||||
|
|
||||||
subs =
|
|
||||||
subs ++
|
|
||||||
Enum.map(mentions, fn {match, %Actor{id: id, url: url}, uuid} ->
|
|
||||||
short_match = String.split(match, "@") |> tl() |> hd()
|
|
||||||
|
|
||||||
{uuid,
|
|
||||||
"<span><a data-user='#{id}' class='mention' href='#{url}'>@<span>#{short_match}</span></a></span>"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
{subs, uuid_text}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc "Adds the hashtag links"
|
|
||||||
def add_hashtag_links({subs, text}, tags) do
|
|
||||||
tags =
|
|
||||||
tags
|
|
||||||
|> Enum.sort_by(fn {name, _} -> -String.length(name) end)
|
|
||||||
|> Enum.map(fn {name, short} -> {name, short, Ecto.UUID.generate()} end)
|
|
||||||
|
|
||||||
uuid_text =
|
|
||||||
tags
|
|
||||||
|> Enum.reduce(text, fn {match, _short, uuid}, text ->
|
|
||||||
String.replace(text, match, uuid)
|
|
||||||
end)
|
|
||||||
|
|
||||||
subs =
|
|
||||||
subs ++
|
|
||||||
Enum.map(tags, fn {tag_text, tag, uuid} ->
|
|
||||||
url =
|
|
||||||
"<a data-tag='#{tag}' href='#{MobilizonWeb.Endpoint.url()}/tag/#{tag}' rel='tag'>#{
|
|
||||||
tag_text
|
|
||||||
}</a>"
|
|
||||||
|
|
||||||
{uuid, url}
|
|
||||||
end)
|
|
||||||
|
|
||||||
{subs, uuid_text}
|
|
||||||
end
|
|
||||||
|
|
||||||
def finalize({subs, text}) do
|
|
||||||
Enum.reduce(subs, text, fn {uuid, replacement}, result_text ->
|
|
||||||
String.replace(result_text, uuid, replacement)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -90,6 +90,9 @@ defmodule Mobilizon.Mixfile do
|
||||||
{:earmark, "~> 1.3.1"},
|
{:earmark, "~> 1.3.1"},
|
||||||
{:geohax, "~> 0.3.0"},
|
{:geohax, "~> 0.3.0"},
|
||||||
{:mogrify, "~> 0.7.2"},
|
{:mogrify, "~> 0.7.2"},
|
||||||
|
{:auto_linker,
|
||||||
|
git: "https://git.pleroma.social/pleroma/auto_linker.git",
|
||||||
|
ref: "95e8188490e97505c56636c1379ffdf036c1fdde"},
|
||||||
# Dev and test dependencies
|
# Dev and test dependencies
|
||||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
{:ex_machina, "~> 2.3", only: [:dev, :test]},
|
{:ex_machina, "~> 2.3", only: [:dev, :test]},
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -7,6 +7,7 @@
|
||||||
"arc_ecto": {:git, "https://github.com/tcitworld/arc_ecto.git", "e0d8db119c564744404cff68157417e2a83941af", []},
|
"arc_ecto": {:git, "https://github.com/tcitworld/arc_ecto.git", "e0d8db119c564744404cff68157417e2a83941af", []},
|
||||||
"argon2_elixir": {:hex, :argon2_elixir, "2.0.5", "0073a87d755c7e63fc4f9d08b1d1646585b93f144cecde126e15061b24240b20", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
"argon2_elixir": {:hex, :argon2_elixir, "2.0.5", "0073a87d755c7e63fc4f9d08b1d1646585b93f144cecde126e15061b24240b20", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"atomex": {:hex, :atomex, "0.3.0", "19b5d1a2aef8706dbd307385f7d5d9f6f273869226d317492c396c7bacf26402", [:mix], [{:xml_builder, "~> 2.0.0", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm"},
|
"atomex": {:hex, :atomex, "0.3.0", "19b5d1a2aef8706dbd307385f7d5d9f6f273869226d317492c396c7bacf26402", [:mix], [{:xml_builder, "~> 2.0.0", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "95e8188490e97505c56636c1379ffdf036c1fdde", [ref: "95e8188490e97505c56636c1379ffdf036c1fdde"]},
|
||||||
"bamboo": {:hex, :bamboo, "1.2.0", "8aebd24f7c606c32d0163c398004a11608ca1028182a169b2e527793bfab7561", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
|
"bamboo": {:hex, :bamboo, "1.2.0", "8aebd24f7c606c32d0163c398004a11608ca1028182a169b2e527793bfab7561", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
"bamboo_smtp": {:hex, :bamboo_smtp, "1.7.0", "f0d213e18ced1f08b551a72221e9b8cfbf23d592b684e9aa1ef5250f4943ef9b", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.14.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"},
|
"bamboo_smtp": {:hex, :bamboo_smtp, "1.7.0", "f0d213e18ced1f08b551a72221e9b8cfbf23d592b684e9aa1ef5250f4943ef9b", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.14.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
|
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
|
||||||
|
|
1595
schema.graphql
1595
schema.graphql
File diff suppressed because it is too large
Load diff
205
test/mobilizon/service/formatter/formatter_test.exs
Normal file
205
test/mobilizon/service/formatter/formatter_test.exs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Mobilizon.Service.FormatterTest do
|
||||||
|
alias Mobilizon.Service.Formatter
|
||||||
|
use Mobilizon.DataCase
|
||||||
|
|
||||||
|
import Mobilizon.Factory
|
||||||
|
|
||||||
|
describe ".add_hashtag_links" do
|
||||||
|
test "turns hashtags into links" do
|
||||||
|
text = "I love #cofe and #2hu"
|
||||||
|
|
||||||
|
expected_text =
|
||||||
|
"I love <a class='hashtag' data-tag='cofe' href='http://mobilizon.test/tag/cofe' rel='tag'>#cofe</a> and <a class='hashtag' data-tag='2hu' href='http://mobilizon.test/tag/2hu' rel='tag'>#2hu</a>"
|
||||||
|
|
||||||
|
assert {^expected_text, [], _tags} = Formatter.linkify(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not turn html characters to tags" do
|
||||||
|
text = "#fact_3: pleroma does what mastodon't"
|
||||||
|
|
||||||
|
expected_text =
|
||||||
|
"<a class='hashtag' data-tag='fact_3' href='http://mobilizon.test/tag/fact_3' rel='tag'>#fact_3</a>: pleroma does what mastodon't"
|
||||||
|
|
||||||
|
assert {^expected_text, [], _tags} = Formatter.linkify(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".add_links" do
|
||||||
|
test "turning urls into links" do
|
||||||
|
text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
|
||||||
|
|
||||||
|
expected =
|
||||||
|
"Hey, check out <a href=\"https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla\">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> ."
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "https://mastodon.social/@lambadalambda"
|
||||||
|
|
||||||
|
expected =
|
||||||
|
"<a href=\"https://mastodon.social/@lambadalambda\">https://mastodon.social/@lambadalambda</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "https://mastodon.social:4000/@lambadalambda"
|
||||||
|
|
||||||
|
expected =
|
||||||
|
"<a href=\"https://mastodon.social:4000/@lambadalambda\">https://mastodon.social:4000/@lambadalambda</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "@lambadalambda"
|
||||||
|
expected = "@lambadalambda"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "http://www.cs.vu.nl/~ast/intel/"
|
||||||
|
expected = "<a href=\"http://www.cs.vu.nl/~ast/intel/\">http://www.cs.vu.nl/~ast/intel/</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
|
||||||
|
|
||||||
|
expected =
|
||||||
|
"<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
|
||||||
|
|
||||||
|
expected =
|
||||||
|
"<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
|
||||||
|
|
||||||
|
expected =
|
||||||
|
"<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "https://en.wikipedia.org/wiki/Duff's_device"
|
||||||
|
|
||||||
|
expected =
|
||||||
|
"<a href=\"https://en.wikipedia.org/wiki/Duff's_device\">https://en.wikipedia.org/wiki/Duff's_device</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "https://pleroma.com https://pleroma.com/sucks"
|
||||||
|
|
||||||
|
expected =
|
||||||
|
"<a href=\"https://pleroma.com\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\">https://pleroma.com/sucks</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text = "xmpp:contact@hacktivis.me"
|
||||||
|
|
||||||
|
expected = "<a href=\"xmpp:contact@hacktivis.me\">xmpp:contact@hacktivis.me</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
text =
|
||||||
|
"magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
|
||||||
|
|
||||||
|
expected = "<a href=\"#{text}\">#{text}</a>"
|
||||||
|
|
||||||
|
assert {^expected, [], []} = Formatter.linkify(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "add_user_links" do
|
||||||
|
test "gives a replacement for user links, using local nicknames in user links text" do
|
||||||
|
text = "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme@archae.me"
|
||||||
|
gsimg = insert(:actor, preferred_username: "gsimg")
|
||||||
|
|
||||||
|
archaeme =
|
||||||
|
insert(:actor, preferred_username: "archa_eme_", url: "https://archeme/@archa_eme_")
|
||||||
|
|
||||||
|
archaeme_remote = insert(:actor, preferred_username: "archaeme", domain: "archae.me")
|
||||||
|
|
||||||
|
{text, mentions, []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
assert length(mentions) == 3
|
||||||
|
|
||||||
|
expected_text =
|
||||||
|
"<span class='h-card'><a data-user='#{gsimg.id}' class='u-url mention' href='#{gsimg.url}'>@<span>gsimg</span></a></span> According to <span class='h-card'><a data-user='#{
|
||||||
|
archaeme.id
|
||||||
|
}' class='u-url mention' href='#{"https://archeme/@archa_eme_"}'>@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class='h-card'><a data-user='#{
|
||||||
|
archaeme_remote.id
|
||||||
|
}' class='u-url mention' href='#{archaeme_remote.url}'>@<span>archaeme</span></a></span>"
|
||||||
|
|
||||||
|
assert expected_text == text
|
||||||
|
end
|
||||||
|
|
||||||
|
test "gives a replacement for single-character local nicknames" do
|
||||||
|
text = "@o hi"
|
||||||
|
o = insert(:actor, preferred_username: "o")
|
||||||
|
|
||||||
|
{text, mentions, []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
assert length(mentions) == 1
|
||||||
|
|
||||||
|
expected_text =
|
||||||
|
"<span class='h-card'><a data-user='#{o.id}' class='u-url mention' href='#{o.url}'>@<span>o</span></a></span> hi"
|
||||||
|
|
||||||
|
assert expected_text == text
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not give a replacement for single-character local nicknames who don't exist" do
|
||||||
|
text = "@a hi"
|
||||||
|
|
||||||
|
expected_text = "@a hi"
|
||||||
|
assert {^expected_text, [] = _mentions, [] = _tags} = Formatter.linkify(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".parse_tags" do
|
||||||
|
test "parses tags in the text" do
|
||||||
|
text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。"
|
||||||
|
|
||||||
|
expected_tags = [
|
||||||
|
{"#Test", "test"},
|
||||||
|
{"#working", "working"},
|
||||||
|
{"#は", "は"},
|
||||||
|
{"#漢字", "漢字"}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert {_text, [], ^expected_tags} = Formatter.linkify(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it can parse mentions and return the relevant users" do
|
||||||
|
text =
|
||||||
|
"@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm"
|
||||||
|
|
||||||
|
o = insert(:actor, preferred_username: "o")
|
||||||
|
jimm = insert(:actor, preferred_username: "jimm")
|
||||||
|
gsimg = insert(:actor, preferred_username: "gsimg")
|
||||||
|
archaeme = insert(:actor, preferred_username: "archaeme")
|
||||||
|
archaeme_remote = insert(:actor, preferred_username: "archaeme", domain: "archae.me")
|
||||||
|
|
||||||
|
expected_mentions = [
|
||||||
|
{"@archaeme", archaeme.id},
|
||||||
|
{"@archaeme@archae.me", archaeme_remote.id},
|
||||||
|
{"@gsimg", gsimg.id},
|
||||||
|
{"@jimm", jimm.id},
|
||||||
|
{"@o", o.id}
|
||||||
|
]
|
||||||
|
|
||||||
|
{_text, mentions, []} = Formatter.linkify(text)
|
||||||
|
|
||||||
|
assert expected_mentions ==
|
||||||
|
Enum.map(mentions, fn {username, actor} -> {username, actor.id} end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it escapes HTML in plain text" do
|
||||||
|
text = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
|
||||||
|
expected = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
|
||||||
|
|
||||||
|
assert Formatter.html_escape(text, "text/plain") == expected
|
||||||
|
end
|
||||||
|
end
|
|
@ -84,6 +84,44 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
|
||||||
assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event"
|
assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "create_event/3 creates an event with tags", %{conn: conn, actor: actor, user: user} do
|
||||||
|
mutation = """
|
||||||
|
mutation {
|
||||||
|
createEvent(
|
||||||
|
title: "my event is referenced",
|
||||||
|
description: "with tags!",
|
||||||
|
begins_on: "#{
|
||||||
|
DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
|
||||||
|
}",
|
||||||
|
organizer_actor_id: "#{actor.id}",
|
||||||
|
category: "birthday",
|
||||||
|
tags: ["nicolas", "birthday", "bad tag"]
|
||||||
|
) {
|
||||||
|
title,
|
||||||
|
uuid,
|
||||||
|
tags {
|
||||||
|
title,
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||||
|
|
||||||
|
assert json_response(res, 200)["errors"] == nil
|
||||||
|
assert json_response(res, 200)["data"]["createEvent"]["title"] == "my event is referenced"
|
||||||
|
|
||||||
|
assert json_response(res, 200)["data"]["createEvent"]["tags"] == [
|
||||||
|
%{"slug" => "nicolas", "title" => "nicolas"},
|
||||||
|
%{"slug" => "birthday", "title" => "birthday"},
|
||||||
|
%{"slug" => "bad-tag", "title" => "bad tag"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
test "create_event/3 creates an event with an attached picture", %{
|
test "create_event/3 creates an event with an attached picture", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
actor: actor,
|
actor: actor,
|
||||||
|
|
Loading…
Reference in a new issue