diff --git a/config/config.exs b/config/config.exs index e4e4aaad2..6f1f25061 100644 --- a/config/config.exs +++ b/config/config.exs @@ -163,6 +163,8 @@ config :auto_linker, rel: "noopener noreferrer ugc" ] +config :tesla, adapter: Tesla.Adapter.Hackney + config :phoenix, :format_encoders, json: Jason, "activity-json": Jason config :phoenix, :json_library, Jason diff --git a/config/test.exs b/config/test.exs index 6f8462ca5..2c8b466f3 100644 --- a/config/test.exs +++ b/config/test.exs @@ -44,6 +44,11 @@ config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "test/uploads" config :exvcr, vcr_cassette_library_dir: "test/fixtures/vcr_cassettes" +config :tesla, Mobilizon.Service.HTTP.ActivityPub, + adapter: Mobilizon.Service.HTTP.ActivityPub.Mock + +config :tesla, Mobilizon.Service.HTTP.BaseClient, adapter: Mobilizon.Service.HTTP.BaseClient.Mock + config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock config :mobilizon, Oban, queues: false, prune: :disabled, crontab: false diff --git a/js/package.json b/js/package.json index 43d57fa04..97609c267 100644 --- a/js/package.json +++ b/js/package.json @@ -49,7 +49,7 @@ "vue-router": "^3.1.6", "vue-scrollto": "^2.17.1", "vue2-leaflet": "^2.0.3", - "vuedraggable": "^2.23.2" + "vuedraggable": "2.23.2" }, "devDependencies": { "@types/chai": "^4.2.11", @@ -90,7 +90,7 @@ "prettier-eslint": "^10.1.1", "sass-loader": "^8.0.2", "typescript": "~3.9.3", - "vue-cli-plugin-styleguidist": "~4.26.0", + "vue-cli-plugin-styleguidist": "~4.29.1", "vue-cli-plugin-svg": "~0.1.3", "vue-i18n-extract": "^1.0.2", "vue-template-compiler": "^2.6.11", diff --git a/js/src/App.vue b/js/src/App.vue index ec0c54f59..55b2f2310 100644 --- a/js/src/App.vue +++ b/js/src/App.vue @@ -59,6 +59,7 @@ import { initializeCurrentActor } from "./utils/auth"; import { CONFIG } from "./graphql/config"; import { IConfig } from "./types/config.model"; import { ICurrentUser } from "./types/current-user.model"; + @Component({ apollo: { currentUser: CURRENT_USER_CLIENT, @@ -72,6 +73,7 @@ import { ICurrentUser } from "./types/current-user.model"; }) export default class App extends Vue { config!: IConfig; + currentUser!: ICurrentUser; async created() { diff --git a/js/src/components/Comment/Comment.vue b/js/src/components/Comment/Comment.vue index 07f0b383e..e2b7be14f 100644 --- a/js/src/components/Comment/Comment.vue +++ b/js/src/components/Comment/Comment.vue @@ -138,7 +138,7 @@ import { IEvent, CommentModeration } from "../../types/event.model"; import ReportModal from "../Report/ReportModal.vue"; import { IReport } from "../../types/report.model"; import { CREATE_REPORT } from "../../graphql/report"; -import PopoverActorCard from "../../components/Account/PopoverActorCard.vue"; +import PopoverActorCard from "../Account/PopoverActorCard.vue"; @Component({ apollo: { diff --git a/js/src/components/Conversation/ConversationComment.vue b/js/src/components/Discussion/DiscussionComment.vue similarity index 87% rename from js/src/components/Conversation/ConversationComment.vue rename to js/src/components/Discussion/DiscussionComment.vue index 93ec1954c..6724dc4d6 100644 --- a/js/src/components/Conversation/ConversationComment.vue +++ b/js/src/components/Discussion/DiscussionComment.vue @@ -12,7 +12,9 @@ @{{ comment.actor.preferredUsername }}
- {{ comment.updatedAt | formatDateTimeString }} + + {{ $timeAgo.format(comment.insertedAt, "twitter") || $t("Right now") }}
@@ -21,10 +23,10 @@ diff --git a/js/src/components/Conversation/ConversationListItem.vue b/js/src/components/Discussion/DiscussionListItem.vue similarity index 57% rename from js/src/components/Conversation/ConversationListItem.vue rename to js/src/components/Discussion/DiscussionListItem.vue index a0949d6f2..5f254af10 100644 --- a/js/src/components/Conversation/ConversationListItem.vue +++ b/js/src/components/Discussion/DiscussionListItem.vue @@ -1,42 +1,45 @@ diff --git a/js/src/components/Resource/FolderItem.vue b/js/src/components/Resource/FolderItem.vue index e254c9224..8e8cdf3a5 100644 --- a/js/src/components/Resource/FolderItem.vue +++ b/js/src/components/Resource/FolderItem.vue @@ -142,7 +142,7 @@ a { position: relative; .preview { - flex: 0 0 100px; + flex: 0 0 50px; position: relative; display: flex; align-items: center; @@ -159,7 +159,7 @@ a { display: block; font-weight: 500; margin-bottom: 5px; - color: $background-color; + color: $primary; overflow: hidden; text-overflow: ellipsis; text-decoration: none; diff --git a/js/src/components/Resource/ResourceItem.vue b/js/src/components/Resource/ResourceItem.vue index c5865ed0d..fcb8e84fa 100644 --- a/js/src/components/Resource/ResourceItem.vue +++ b/js/src/components/Resource/ResourceItem.vue @@ -81,7 +81,7 @@ a { flex: 1; .preview { - flex: 0 0 100px; + flex: 0 0 50px; position: relative; display: flex; align-items: center; diff --git a/js/src/components/Resource/ResourceSelector.vue b/js/src/components/Resource/ResourceSelector.vue index a16bb3be1..6cc5ebd8e 100644 --- a/js/src/components/Resource/ResourceSelector.vue +++ b/js/src/components/Resource/ResourceSelector.vue @@ -76,6 +76,7 @@ import { IResource } from "../../types/resource"; }) export default class ResourceSelector extends Vue { @Prop({ required: true }) initialResource!: IResource; + @Prop({ required: true }) username!: string; resource: IResource | undefined = this.initialResource.parent; diff --git a/js/src/components/Settings/SettingMenuItem.vue b/js/src/components/Settings/SettingMenuItem.vue index ef61a6afe..73a07a157 100644 --- a/js/src/components/Settings/SettingMenuItem.vue +++ b/js/src/components/Settings/SettingMenuItem.vue @@ -13,6 +13,7 @@ import { Route } from "vue-router"; @Component export default class SettingMenuItem extends Vue { @Prop({ required: false, type: String }) title!: string; + @Prop({ required: true, type: Object }) to!: Route; get isActive() { diff --git a/js/src/components/Settings/SettingMenuSection.vue b/js/src/components/Settings/SettingMenuSection.vue index 012be76f4..5eb5d8946 100644 --- a/js/src/components/Settings/SettingMenuSection.vue +++ b/js/src/components/Settings/SettingMenuSection.vue @@ -11,11 +11,13 @@ import { Component, Prop, Vue } from "vue-property-decorator"; import SettingMenuItem from "@/components/Settings/SettingMenuItem.vue"; import { Route } from "vue-router"; + @Component({ components: { SettingMenuItem }, }) export default class SettingMenuSection extends Vue { @Prop({ required: false, type: String }) title!: string; + @Prop({ required: true, type: Object }) to!: Route; get sectionActive() { diff --git a/js/src/components/Settings/SettingsMenu.vue b/js/src/components/Settings/SettingsMenu.vue index 845d3976a..a901edddc 100644 --- a/js/src/components/Settings/SettingsMenu.vue +++ b/js/src/components/Settings/SettingsMenu.vue @@ -63,6 +63,7 @@ import { CURRENT_USER_CLIENT } from "../../graphql/user"; import { ICurrentUser, ICurrentUserRole } from "../../types/current-user.model"; import RouteName from "../../router/name"; + @Component({ components: { SettingMenuSection, SettingMenuItem }, apollo: { diff --git a/js/src/components/Todo/FullTodo.vue b/js/src/components/Todo/FullTodo.vue index e628bbcee..b9ea52fa3 100644 --- a/js/src/components/Todo/FullTodo.vue +++ b/js/src/components/Todo/FullTodo.vue @@ -24,6 +24,7 @@ import RouteName from "../../router/name"; import { UPDATE_TODO } from "../../graphql/todos"; import ActorAutoComplete from "../Account/ActorAutoComplete.vue"; import { IPerson } from "../../types/actor"; + @Component({ components: { ActorAutoComplete }, }) diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 981d2a87d..5b6afa21a 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -1,6 +1,7 @@ import gql from "graphql-tag"; -import { CONVERSATION_BASIC_FIELDS_FRAGMENT } from "@/graphql/conversation"; +import { DISCUSSION_BASIC_FIELDS_FRAGMENT } from "@/graphql/discussion"; import { RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT } from "@/graphql/resources"; +import { POST_BASIC_FIELDS } from "./post"; export const FETCH_PERSON = gql` query($username: String!) { @@ -479,10 +480,16 @@ export const FETCH_GROUP = gql` } total } - conversations { + discussions { total elements { - ...ConversationBasicFields + ...DiscussionBasicFields + } + } + posts { + total + elements { + ...PostBasicFields } } members { @@ -497,6 +504,7 @@ export const FETCH_GROUP = gql` url } } + insertedAt } total } @@ -537,9 +545,11 @@ export const FETCH_GROUP = gql` } } } - ${CONVERSATION_BASIC_FIELDS_FRAGMENT} + ${DISCUSSION_BASIC_FIELDS_FRAGMENT} + ${POST_BASIC_FIELDS} ${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT} `; + export const CREATE_GROUP = gql` mutation CreateGroup( $creatorActorId: ID! @@ -571,6 +581,29 @@ export const CREATE_GROUP = gql` } `; +export const UPDATE_GROUP = gql` + mutation UpdateGroup( + $id: ID! + $name: String + $summary: String + $avatar: PictureInput + $banner: PictureInput + ) { + createGroup(id: $id, name: $name, summary: $summary, banner: $banner, avatar: $avatar) { + id + preferredUsername + name + summary + avatar { + url + } + banner { + url + } + } + } +`; + export const SUSPEND_PROFILE = gql` mutation SuspendProfile($id: ID!) { suspendProfile(id: $id) { diff --git a/js/src/graphql/conversation.ts b/js/src/graphql/conversation.ts deleted file mode 100644 index f44a47a3d..000000000 --- a/js/src/graphql/conversation.ts +++ /dev/null @@ -1,120 +0,0 @@ -import gql from "graphql-tag"; - -export const CONVERSATION_BASIC_FIELDS_FRAGMENT = gql` - fragment ConversationBasicFields on Conversation { - id - title - slug - lastComment { - id - text - actor { - preferredUsername - avatar { - url - } - } - } - } -`; - -export const CONVERSATION_FIELDS_FOR_REPLY_FRAGMENT = gql` - fragment ConversationFieldsReply on Conversation { - id - title - slug - lastComment { - id - text - updatedAt - actor { - id - preferredUsername - avatar { - url - } - } - } - actor { - id - preferredUsername - } - creator { - id - preferredUsername - } - } -`; - -export const CONVERSATION_FIELDS_FRAGMENT = gql` - fragment ConversationFields on Conversation { - id - title - slug - lastComment { - id - text - updatedAt - } - actor { - id - preferredUsername - } - creator { - id - preferredUsername - } - } -`; - -export const CREATE_CONVERSATION = gql` - mutation createConversation($title: String!, $creatorId: ID!, $actorId: ID!, $text: String!) { - createConversation(title: $title, text: $text, creatorId: $creatorId, actorId: $actorId) { - ...ConversationFields - } - } - ${CONVERSATION_FIELDS_FRAGMENT} -`; - -export const REPLY_TO_CONVERSATION = gql` - mutation replyToConversation($conversationId: ID!, $text: String!) { - replyToConversation(conversationId: $conversationId, text: $text) { - ...ConversationFieldsReply - } - } - ${CONVERSATION_FIELDS_FOR_REPLY_FRAGMENT} -`; - -export const GET_CONVERSATION = gql` - query getConversation($id: ID!, $page: Int, $limit: Int) { - conversation(id: $id) { - comments(page: $page, limit: $limit) { - total - elements { - id - text - actor { - id - avatar { - url - } - preferredUsername - } - insertedAt - updatedAt - } - } - ...ConversationFields - } - } - ${CONVERSATION_FIELDS_FRAGMENT} -`; - -export const UPDATE_CONVERSATION = gql` - mutation updateConversation($conversationId: ID!, $title: String!) { - updateConversation(conversationId: $conversationId, title: $title) { - ...ConversationFields - } - } - ${CONVERSATION_FIELDS_FRAGMENT} -`; diff --git a/js/src/graphql/discussion.ts b/js/src/graphql/discussion.ts new file mode 100644 index 000000000..035aa83db --- /dev/null +++ b/js/src/graphql/discussion.ts @@ -0,0 +1,158 @@ +import gql from "graphql-tag"; + +export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql` + fragment DiscussionBasicFields on Discussion { + id + title + slug + lastComment { + id + text + actor { + id + preferredUsername + avatar { + url + } + } + } + } +`; + +export const DISCUSSION_FIELDS_FOR_REPLY_FRAGMENT = gql` + fragment DiscussionFieldsReply on Discussion { + id + title + slug + lastComment { + id + text + updatedAt + actor { + id + preferredUsername + avatar { + url + } + } + } + actor { + id + preferredUsername + } + creator { + id + preferredUsername + } + } +`; + +export const DISCUSSION_FIELDS_FRAGMENT = gql` + fragment DiscussionFields on Discussion { + id + title + slug + lastComment { + id + text + updatedAt + } + actor { + id + domain + name + preferredUsername + } + creator { + id + domain + name + preferredUsername + } + } +`; + +export const CREATE_DISCUSSION = gql` + mutation createDiscussion($title: String!, $creatorId: ID!, $actorId: ID!, $text: String!) { + createDiscussion(title: $title, text: $text, creatorId: $creatorId, actorId: $actorId) { + ...DiscussionFields + } + } + ${DISCUSSION_FIELDS_FRAGMENT} +`; + +export const REPLY_TO_DISCUSSION = gql` + mutation replyToDiscussion($discussionId: ID!, $text: String!) { + replyToDiscussion(discussionId: $discussionId, text: $text) { + ...DiscussionFields + } + } + ${DISCUSSION_FIELDS_FRAGMENT} +`; + +export const GET_DISCUSSION = gql` + query getDiscussion($slug: String!, $page: Int, $limit: Int) { + discussion(slug: $slug) { + comments(page: $page, limit: $limit) + @connection(key: "discussion-comments", filter: ["slug"]) { + total + elements { + id + text + actor { + id + avatar { + url + } + name + domain + preferredUsername + } + insertedAt + updatedAt + } + } + ...DiscussionFields + } + } + ${DISCUSSION_FIELDS_FRAGMENT} +`; + +export const UPDATE_DISCUSSION = gql` + mutation updateDiscussion($discussionId: ID!, $title: String!) { + updateDiscussion(discussionId: $discussionId, title: $title) { + ...DiscussionFields + } + } + ${DISCUSSION_FIELDS_FRAGMENT} +`; + +export const DELETE_DISCUSSION = gql` + mutation deleteDiscussion($discussionId: ID!) { + deleteDiscussion(discussionId: $discussionId) { + id + } + } +`; + +export const DISCUSSION_COMMENT_CHANGED = gql` + subscription($slug: String!) { + discussionCommentChanged(slug: $slug) { + id + lastComment { + id + text + updatedAt + insertedAt + actor { + id + preferredUsername + domain + avatar { + url + } + } + } + } + } +`; diff --git a/js/src/graphql/member.ts b/js/src/graphql/member.ts index 13778e17e..2331cc35d 100644 --- a/js/src/graphql/member.ts +++ b/js/src/graphql/member.ts @@ -22,3 +22,31 @@ export const ACCEPT_INVITATION = gql` } } `; + +export const GROUP_MEMBERS = gql` + query($name: String!, $roles: String, $page: Int, $limit: Int) { + group(preferredUsername: $name) { + id + url + name + domain + preferredUsername + members(page: $page, limit: $limit, roles: $roles) { + elements { + role + actor { + id + name + domain + preferredUsername + avatar { + url + } + } + insertedAt + } + total + } + } + } +`; diff --git a/js/src/graphql/post.ts b/js/src/graphql/post.ts new file mode 100644 index 000000000..5f81570ff --- /dev/null +++ b/js/src/graphql/post.ts @@ -0,0 +1,151 @@ +import gql from "graphql-tag"; +import { TAG_FRAGMENT } from "./tags"; + +export const POST_FRAGMENT = gql` + fragment PostFragment on Post { + id + title + slug + url + body + author { + id + preferredUsername + name + domain + avatar { + url + } + } + attributedTo { + id + preferredUsername + name + domain + avatar { + url + } + } + insertedAt + updatedAt + publishAt + draft + visibility + tags { + ...TagFragment + } + } + ${TAG_FRAGMENT} +`; + +export const POST_BASIC_FIELDS = gql` + fragment PostBasicFields on Post { + id + title + slug + url + author { + id + preferredUsername + name + avatar { + url + } + } + attributedTo { + id + preferredUsername + name + avatar { + url + } + } + insertedAt + updatedAt + publishAt + draft + } +`; + +export const FETCH_GROUP_POSTS = gql` + query GroupPosts($preferredUsername: String!, $page: Int, $limit: Int) { + group(preferredUsername: $preferredUsername) { + id + preferredUsername + domain + name + posts(page: $page, limit: $limit) { + total + elements { + ...PostBasicFields + } + } + } + } + ${POST_BASIC_FIELDS} +`; + +export const FETCH_POST = gql` + query Post($slug: String!) { + post(slug: $slug) { + ...PostFragment + } + } + ${POST_FRAGMENT} +`; + +export const CREATE_POST = gql` + mutation CreatePost( + $title: String! + $body: String + $attributedToId: ID! + $visibility: PostVisibility + $draft: Boolean + $tags: [String] + ) { + createPost( + title: $title + body: $body + attributedToId: $attributedToId + visibility: $visibility + draft: $draft + tags: $tags + ) { + ...PostFragment + } + } + ${POST_FRAGMENT} +`; + +export const UPDATE_POST = gql` + mutation UpdatePost( + $id: ID! + $title: String + $body: String + $attributedToId: ID + $visibility: PostVisibility + $draft: Boolean + $tags: [String] + ) { + updatePost( + id: $id + title: $title + body: $body + attributedToId: $attributedToId + visibility: $visibility + draft: $draft + tags: $tags + ) { + ...PostFragment + } + } + ${POST_FRAGMENT} +`; + +export const DELETE_POST = gql` + mutation DeletePost($id: ID!) { + deletePost(id: $id) { + id + } + } +`; diff --git a/js/src/graphql/tags.ts b/js/src/graphql/tags.ts index 53c4deae7..e59b4428c 100644 --- a/js/src/graphql/tags.ts +++ b/js/src/graphql/tags.ts @@ -1,6 +1,13 @@ import gql from "graphql-tag"; -/* eslint-disable import/prefer-default-export */ +export const TAG_FRAGMENT = gql` + fragment TagFragment on Tag { + id + slug + title + } +`; + export const TAGS = gql` query { tags { diff --git a/js/src/i18n/ar.json b/js/src/i18n/ar.json index 58eecf3cb..cd8b8479b 100644 --- a/js/src/i18n/ar.json +++ b/js/src/i18n/ar.json @@ -55,7 +55,7 @@ "Continue editing": "مواصلة التحرير", "Country": "البلد", "Create": "انشاء", - "Create a new conversation": "أنشئ محادثة جديدة", + "Create a new discussion": "أنشئ محادثة جديدة", "Create a new event": "انشاء فعالية جديدة", "Create a new group": "إنشاء فريق جديد", "Create a new identity": "إنشاء هوية جديدة", @@ -186,7 +186,7 @@ "My events": "فعالياتي", "My identities": "هوياتي", "Name": "الإسم", - "New conversation": "محادثة جديدة", + "New discussion": "محادثة جديدة", "New email": "العنوان الجديد للبريد الإلكتروني", "New folder": "مجلد جديد", "New link": "رابط جديد", diff --git a/js/src/i18n/be.json b/js/src/i18n/be.json index c52ea12aa..e997157a9 100644 --- a/js/src/i18n/be.json +++ b/js/src/i18n/be.json @@ -21,7 +21,7 @@ "An error has occurred.": "Адбылася памылка.", "Approve": "Пацвердзіць", "Are you sure you want to delete this comment? This action cannot be undone.": "Вы сапраўды хочаце выдаліць гэты каментарый? Гэта дзеянне нельга адмяніць.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Вы сапраўды жадаеце выдаліць гэту падзею? Гэта дзеянне нельга адмяніць. Магчыма, варта замест гэтага пагаварыць з аўтарам ці аўтаркай падзеі ці адрэдагаваць падзею.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Вы сапраўды жадаеце выдаліць гэту падзею? Гэта дзеянне нельга адмяніць. Магчыма, варта замест гэтага пагаварыць з аўтарам ці аўтаркай падзеі ці адрэдагаваць падзею.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Вы сапраўды хочаце адмяніць стварэнне падзеі? Вы страціце ўсе свае рэдагаванні.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Вы сапраўды хочаце адмяніць рэдагаванне падзеі? Вы страціце ўсе рэдагаванні.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Вы сапраўды хочаце адмовіцца ад удзелу ў падзеі «{title}»?", diff --git a/js/src/i18n/ca.json b/js/src/i18n/ca.json index 64bf278dd..ff36b7bd2 100644 --- a/js/src/i18n/ca.json +++ b/js/src/i18n/ca.json @@ -30,7 +30,7 @@ "Approve": "Aprova", "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Segur que voleu suprimir tot el compte? Ho perdràs tot. Les identitats, la configuració, els esdeveniments creats, els missatges i les participacions desapareixeran per sempre.", "Are you sure you want to delete this comment? This action cannot be undone.": "Segur que vols esborrar aquest comentari? Aquesta acció és irreversible.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Segur que vols esborrar aquesta activitat? Aquesta acció és irreversible. En comptes d'això, pots parlar amb la persona creadora de l'activitat o modificar l'activitat.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Segur que vols esborrar aquesta activitat? Aquesta acció és irreversible. En comptes d'això, pots parlar amb la persona creadora de l'activitat o modificar l'activitat.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Segur que vols esborrar aquesta activitat? Perdràs tots els canvis.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Segur que vols canceŀlar l'edició? Perdràs tots els canvis que hagis fet.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Segur que vols deixar de participar a l'activitat \"{title}\"?", diff --git a/js/src/i18n/de.json b/js/src/i18n/de.json index 2aef89866..83a5c7cc1 100644 --- a/js/src/i18n/de.json +++ b/js/src/i18n/de.json @@ -27,7 +27,7 @@ "Approve": "Bestätigen", "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Bist du dir sicher, dass du den gesamten Account löschen möchtest? Du verlierst dadurch alles. Identitäten, Einstellungen, erstellte Events, Nachrichten, Teilnahmen sind dann für immer verschwunden.", "Are you sure you want to delete this comment? This action cannot be undone.": "Bist du sicher, dass du diesen Kommentar löschen willst? Diese Aktion kann nicht rückgängig gemacht werden.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Bist du sicher, dass du diese Veranstaltung löschen willst? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Bist du sicher, dass du diese Veranstaltung löschen willst? Diese Aktion kann nicht rückgängig gemacht werden.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Bist Du dir sicher, dass du das Erstellen der Veranstaltung abbrechen möchtest? Alle Änderungen werden verloren gehen.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Bist du dir sicher, dass Du die Bearbeitung der Veranstaltung abbrechen möchtest? Alle Änderungen werden verloren gehen.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Bist Du dir sicher, dass Du nicht mehr an der Veranstaltung \"{title}\" teilnehmen möchtest?", diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index b0e997c23..73289bd73 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -29,7 +29,7 @@ "Approve": "Approve", "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.", "Are you sure you want to delete this comment? This action cannot be undone.": "Are you sure you want to delete this comment? This action cannot be undone.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Are you sure you want to cancel the event creation? You'll lose all modifications.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Are you sure you want to cancel the event edition? You'll lose all modifications.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Are you sure you want to cancel your participation at event \"{title}\"?", @@ -710,5 +710,9 @@ "Error while login with {provider}. Retry or login another way.": "Error while login with {provider}. Retry or login another way.", "Error while login with {provider}. This login provider doesn't exist.": "Error while login with {provider}. This login provider doesn't exist.", "This user has been disabled": "This user has been disabled", - "You can't reset your password because you use a 3rd-party auth provider to login.": "You can't reset your password because you use a 3rd-party auth provider to login." + "You can't reset your password because you use a 3rd-party auth provider to login.": "You can't reset your password because you use a 3rd-party auth provider to login.", + "Update post {name}": "Update post {name}", + "Create a new post": "Create a new post", + "Post": "Post", + "By {author}": "By {author}" } diff --git a/js/src/i18n/es.json b/js/src/i18n/es.json index 8a8d950cb..f8c4518ae 100644 --- a/js/src/i18n/es.json +++ b/js/src/i18n/es.json @@ -55,7 +55,7 @@ "Approve": "Aprobar", "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "¿Estás realmente seguro de que deseas eliminar toda tu cuenta? Lo perderás todo. Las identidades, la configuración, los eventos creados, los mensajes y las participaciones desaparecerán para siempre.", "Are you sure you want to delete this comment? This action cannot be undone.": "¿Estás seguro de que quieres eliminar este comentario? Esta acción no se puede deshacer.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "¿Estás seguro de que quieres eliminar este evento? Esta acción no se puede deshacer. Es posible que desee entablar una conversación con el creador del evento o editar el evento en su lugar.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "¿Estás seguro de que quieres eliminar este evento? Esta acción no se puede deshacer. Es posible que desee entablar una conversación con el creador del evento o editar el evento en su lugar.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "¿Seguro que quieres cancelar la creación del evento? Perderás todas las modificaciones.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "¿Seguro que quieres cancelar la edición del evento? Perderás todas las modificaciones.", "Are you sure you want to cancel your participation at event \"{title}\"?": "¿Está seguro de que desea cancelar su participación en el evento \"{title}\"?", @@ -103,14 +103,14 @@ "Confirmed: Will happen": "Confirmado: sucederá", "Contact": "Contacto", "Continue editing": "Continua editando", - "Conversations": "Conversaciones", + "Discussions": "Conversaciones", "Cookies and Local storage": "Cookies y almacenamiento local", "Country": "País", "Create": "Crear", "Create a calc": "Crear un calco", "Create a discussion": "Crear una discusión", "Create a folder": "Crear una carpeta", - "Create a new conversation": "Crea una nueva conversación", + "Create a new discussion": "Crea una nueva conversación", "Create a new event": "Crear un nuevo evento", "Create a new group": "Crear un nuevo grupo", "Create a new identity": "Crear una nueva identidad", @@ -347,7 +347,7 @@ "My identities": "Mis identidades", "NOTE! The default terms have not been checked over by a lawyer and thus are unlikely to provide full legal protection for all situations for an instance admin using them. They are also not specific to all countries and jurisdictions. If you are unsure, please check with a lawyer.": "¡NOTA! Los términos predeterminados no han sido revisados por un abogado y, por lo tanto, es poco probable que brinden protección legal completa para todas las situaciones para un administrador de instancia que los use. Tampoco son específicos de todos los países y jurisdicciones. Si no está seguro, consulte con un abogado.", "Name": "Nombre", - "New conversation": "Nueva conversación", + "New discussion": "Nueva conversación", "New discussion": "Nueva discusión", "New email": "Nuevo correo electrónico", "New folder": "Nueva carpeta", @@ -620,7 +620,7 @@ "Username": "Nombre de usuario", "Users": "Los usuarios", "View a reply": "|Ver una respuesta|Ver {totalReplies} respuestas", - "View all conversations": "Ver todas las conversaciones", + "View all discussions": "Ver todas las conversaciones", "View all discussions": "Ver todas las discusiones", "View all resources": "Ver todos los recursos", "View all todos": "Ver todas las tareas pendientes", diff --git a/js/src/i18n/fi.json b/js/src/i18n/fi.json index cd7289a45..b8e552cd8 100644 --- a/js/src/i18n/fi.json +++ b/js/src/i18n/fi.json @@ -54,7 +54,7 @@ "Approve": "Hyväksy", "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Haluatko varmasti poistaa koko tilin? Tällöin kaikki poistetaan. Identiteetit, asetukset, luodut tapahtumat, viestit ja osallistumiset poistetaan pysyvästi.", "Are you sure you want to delete this comment? This action cannot be undone.": "Haluatko varmasti poistaa tämän kommentin? Toimintoa ei voi perua.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Haluatko varmasti poistaa tämän tapahtuman? Toimintoa ei voi perua. Poistamisen sijaan voisit ehkä keskustella tapahtuman luojan kanssa tai muokata tapahtumaa.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Haluatko varmasti poistaa tämän tapahtuman? Toimintoa ei voi perua. Poistamisen sijaan voisit ehkä keskustella tapahtuman luojan kanssa tai muokata tapahtumaa.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Haluatko varmasti keskeyttää tapahtuman luomisen? Kaikki muutokset menetetään.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Haluatko varmasti keskeyttää tapahtuman muokkaamisen? Kaikki muutokset menetetään.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Haluatko varmasti perua osallistumisesi tapahtumaan {title}?", @@ -101,14 +101,14 @@ "Confirmed: Will happen": "Vahvistettu: Tapahtuu", "Contact": "Ota yhteyttä", "Continue editing": "Jatka muokkausta", - "Conversations": "Keskustelut", + "Discussions": "Keskustelut", "Cookies and Local storage": "Evästeet ja paikallisesti tallennettavat tiedot", "Country": "Maa", "Create": "Luo", "Create a calc": "Luo taulukko", "Create a discussion": "Luo keskustelu", "Create a folder": "Luo kansio", - "Create a new conversation": "Luo uusi keskustelu", + "Create a new discussion": "Luo uusi keskustelu", "Create a new event": "Luo uusi tapahtuma", "Create a new group": "Luo uusi ryhmä", "Create a new identity": "Luo uusi identiteetti", @@ -341,7 +341,7 @@ "My identities": "Omat identiteetit", "NOTE! The default terms have not been checked over by a lawyer and thus are unlikely to provide full legal protection for all situations for an instance admin using them. They are also not specific to all countries and jurisdictions. If you are unsure, please check with a lawyer.": "HUOM! Oletusehdot eivät ole juristin tarkistamia, joten palvelimen ylläpitäjän ei ole syytä luottaa niiden tarjoamaan juridiseen suojaan. Niitä ei ole myöskään sovitettu eri maiden ja lainkäyttöalueiden olosuhteisiin. Epävarmoissa tilanteissa suosittelemme tarkistuttamaan ehdot lakiasiantuntijalla.", "Name": "Nimi", - "New conversation": "Uusi keskustelu", + "New discussion": "Uusi keskustelu", "New discussion": "Uusi keskustelu", "New email": "Uusi sähköpostiosoite", "New folder": "Uusi kansio", @@ -615,7 +615,7 @@ "Username": "Käyttäjänimi", "Users": "Käyttäjät", "View a reply": "|Näytä vastaus|Näytä {totalReplies} vastausta", - "View all conversations": "Näytä kaikki keskustelut", + "View all discussions": "Näytä kaikki keskustelut", "View all discussions": "Näytä kaikki keskustelut", "View all resources": "Näytä kaikki resurssit", "View all todos": "Näytä kaikki tehtävät", diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 031014ba3..ba2783f56 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -53,7 +53,7 @@ "Approve": "Approuver", "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Êtes-vous vraiment certain⋅e de vouloir supprimer votre compte ? Vous allez tout perdre. Identités, paramètres, événements créés, messages et participations disparaîtront pour toujours.", "Are you sure you want to delete this comment? This action cannot be undone.": "Êtes-vous certain⋅e de vouloir supprimer ce commentaire ? Cette action ne peut pas être annulée.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Êtes-vous certain⋅e de vouloir supprimer cet évènement ? Cette action n'est pas réversible. Vous voulez peut-être engager la conversation avec le créateur de l'évènement ou bien modifier son évènement à la place.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Êtes-vous certain⋅e de vouloir supprimer cet évènement ? Cette action n'est pas réversible. Vous voulez peut-être engager la discussion avec le créateur de l'évènement ou bien modifier son évènement à la place.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Étes-vous certain⋅e de vouloir annuler la création de l'évènement ? Vous allez perdre toutes vos modifications.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Êtes-vous certain⋅e de vouloir annuler la modification de l'évènement ? Vous allez perdre toutes vos modifications.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Êtes-vous certain⋅e de vouloir annuler votre participation à l'évènement « {title} » ?", @@ -710,5 +710,9 @@ "Error while login with {provider}. Retry or login another way.": "Erreur lors de la connexion avec {provider}. Réessayez ou bien connectez vous autrement.", "Error while login with {provider}. This login provider doesn't exist.": "Erreur lors de la connexion avec {provider}. Cette méthode de connexion n'existe pas.", "This user has been disabled": "Cet utilisateur·ice a été désactivé·e", - "You can't reset your password because you use a 3rd-party auth provider to login.": "Vous ne pouvez pas réinitialiser votre mot de passe car vous vous connectez via une méthode externe." + "You can't reset your password because you use a 3rd-party auth provider to login.": "Vous ne pouvez pas réinitialiser votre mot de passe car vous vous connectez via une méthode externe.", + "Update post {name}": "Mettre à jour le billet {name}", + "Create a new post": "Créer un nouveau billet", + "Post": "Billet", + "By {author}": "Par {author}" } diff --git a/js/src/i18n/oc.json b/js/src/i18n/oc.json index 1a070b88b..35921974c 100644 --- a/js/src/i18n/oc.json +++ b/js/src/i18n/oc.json @@ -41,7 +41,7 @@ "Are you going to this event?": "Anatz a aqueste eveniment ?", "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Volètz vertadièrament suprimir vòstre compte ? O perdretz tot. Identitats, paramètres, eveniments creats, messatges e participacions desapareisseràn per totjorn.", "Are you sure you want to delete this comment? This action cannot be undone.": "Volètz vertadièrament suprimir aqueste comentari ? Aquesta accion es irreversibla.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Volètz vertadièrament suprimir aqueste eveniment ? Aquesta accion es irreversibla. Benlèu qu’a la plaça volètz començar una conversacion amb l’organizaire o modificar sos eveniment.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Volètz vertadièrament suprimir aqueste eveniment ? Aquesta accion es irreversibla. Benlèu qu’a la plaça volètz començar una conversacion amb l’organizaire o modificar sos eveniment.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Volètz vertadièrament anullar la creacion de l’eveniment ? Perdretz totas vòstras modificacions.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Volètz vertadièrament anullar la modificacion de l’eveniment ? Perdretz totas vòstras modificacions.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Volètz vertadièrament anullar vòstra participacion a l’eveniment « {title} » ?", @@ -84,10 +84,10 @@ "Confirmed: Will happen": "Confirmat : se tendrà", "Contact": "Contacte", "Continue editing": "Contunhar la modificacion", - "Conversations": "Conversacions", + "Discussions": "Conversacions", "Country": "País", "Create": "Crear", - "Create a new conversation": "Crear una conversacion novèla", + "Create a new discussion": "Crear una conversacion novèla", "Create a new event": "Crear un eveniment novèl", "Create a new group": "Crear un grop novèl", "Create a new identity": "Crear una identitat novèla", @@ -273,7 +273,7 @@ "My groups": "Mos grops", "My identities": "Mas identitats", "Name": "Nom", - "New conversation": "Conversacion novèla", + "New discussion": "Conversacion novèla", "New email": "Adreça novèla", "New folder": "Dossièr novèl", "New link": "Ligam novèl", diff --git a/js/src/i18n/pt_BR.json b/js/src/i18n/pt_BR.json index e3e358f48..3b01e5d7e 100644 --- a/js/src/i18n/pt_BR.json +++ b/js/src/i18n/pt_BR.json @@ -26,7 +26,7 @@ "Anonymous participations": "Participações anônimas", "Approve": "Aprovar", "Are you sure you want to delete this comment? This action cannot be undone.": "Você está seguro que quer apagar este comentário? Esta ação não pode ser desfeita.", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Você está seguro que quer apagar este evento? Esta ação não pode ser desfeita. Talvez você queira tentar uma conversa com o criador do evento ou, então, editar este evento.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Você está seguro que quer apagar este evento? Esta ação não pode ser desfeita. Talvez você queira tentar uma conversa com o criador do evento ou, então, editar este evento.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Você está seguro que quer cancelar a criação do evento? Você perderá todas as modificações.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Você está seguro que quer cancelar a edição do evento? Você perderá todas as modificações.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Você está seguro que quer cancelar a sua participação no evento \"{title}\"?", diff --git a/js/src/main.ts b/js/src/main.ts index 5fcc4addb..784e26f8f 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -6,15 +6,29 @@ import Component from "vue-class-component"; import VueScrollTo from "vue-scrollto"; import VueMeta from "vue-meta"; import VTooltip from "v-tooltip"; +import TimeAgo from "javascript-time-ago"; import App from "./App.vue"; import router from "./router"; import { NotifierPlugin } from "./plugins/notifier"; import filters from "./filters"; import { i18n } from "./utils/i18n"; +import messages from "./i18n"; import apolloProvider from "./vue-apollo"; Vue.config.productionTip = false; +let language = document.documentElement.getAttribute("lang") as string; +language = + language || + ((window.navigator as any).userLanguage || window.navigator.language).replace(/-/, "_"); +export const locale = + language && messages.hasOwnProperty(language) ? language : language.split("-")[0]; + +import(`javascript-time-ago/locale/${locale}`).then((localeFile) => { + TimeAgo.addLocale(localeFile); + Vue.prototype.$timeAgo = new TimeAgo(locale); +}); + Vue.use(Buefy); Vue.use(NotifierPlugin); Vue.use(filters); diff --git a/js/src/router/conversation.ts b/js/src/router/conversation.ts deleted file mode 100644 index 7e804c018..000000000 --- a/js/src/router/conversation.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { RouteConfig } from "vue-router"; -import CreateConversation from "@/views/Conversations/Create.vue"; -import ConversationsList from "@/views/Conversations/ConversationsList.vue"; -import Conversation from "@/views/Conversations/Conversation.vue"; - -export enum ConversationRouteName { - CONVERSATION_LIST = "CONVERSATION_LIST", - CREATE_CONVERSATION = "CREATE_CONVERSATION", - CONVERSATION = "CONVERSATION", -} - -export const conversationRoutes: RouteConfig[] = [ - { - path: "/@:preferredUsername/conversations", - name: ConversationRouteName.CONVERSATION_LIST, - component: ConversationsList, - props: true, - meta: { requiredAuth: false }, - }, - { - path: "/@:preferredUsername/conversations/new", - name: ConversationRouteName.CREATE_CONVERSATION, - component: CreateConversation, - props: true, - meta: { requiredAuth: true }, - }, - { - path: "/@:preferredUsername/:slug/:id/:comment_id?", - name: ConversationRouteName.CONVERSATION, - component: Conversation, - props: true, - meta: { requiredAuth: false }, - }, -]; diff --git a/js/src/router/discussion.ts b/js/src/router/discussion.ts new file mode 100644 index 000000000..1767c2845 --- /dev/null +++ b/js/src/router/discussion.ts @@ -0,0 +1,34 @@ +import { RouteConfig } from "vue-router"; +import CreateDiscussion from "@/views/Discussions/Create.vue"; +import DiscussionsList from "@/views/Discussions/DiscussionsList.vue"; +import discussion from "@/views/Discussions/Discussion.vue"; + +export enum DiscussionRouteName { + DISCUSSION_LIST = "DISCUSSION_LIST", + CREATE_DISCUSSION = "CREATE_DISCUSSION", + DISCUSSION = "DISCUSSION", +} + +export const discussionRoutes: RouteConfig[] = [ + { + path: "/@:preferredUsername/discussions", + name: DiscussionRouteName.DISCUSSION_LIST, + component: DiscussionsList, + props: true, + meta: { requiredAuth: false }, + }, + { + path: "/@:preferredUsername/discussions/new", + name: DiscussionRouteName.CREATE_DISCUSSION, + component: CreateDiscussion, + props: true, + meta: { requiredAuth: true }, + }, + { + path: "/@:preferredUsername/c/:slug/:comment_id?", + name: DiscussionRouteName.DISCUSSION, + component: discussion, + props: true, + meta: { requiredAuth: false }, + }, +]; diff --git a/js/src/router/groups.ts b/js/src/router/groups.ts index ec5d693ef..399d7550c 100644 --- a/js/src/router/groups.ts +++ b/js/src/router/groups.ts @@ -1,4 +1,4 @@ -import { RouteConfig } from "vue-router"; +import { RouteConfig, Route } from "vue-router"; export enum GroupsRouteName { TODO_LISTS = "TODO_LISTS", @@ -10,6 +10,10 @@ export enum GroupsRouteName { RESOURCES = "RESOURCES", RESOURCE_FOLDER_ROOT = "RESOURCE_FOLDER_ROOT", RESOURCE_FOLDER = "RESOURCE_FOLDER", + POST_CREATE = "POST_CREATE", + POST_EDIT = "POST_EDIT", + POST = "POST", + POSTS = "POSTS", } const resourceFolder = () => import("@/views/Resources/ResourceFolder.vue"); @@ -61,6 +65,7 @@ export const groupsRoutes: RouteConfig[] = [ { path: "public", name: GroupsRouteName.GROUP_PUBLIC_SETTINGS, + component: () => import("../views/Group/GroupSettings.vue"), }, { path: "members", @@ -70,4 +75,28 @@ export const groupsRoutes: RouteConfig[] = [ }, ], }, + { + path: "/@:preferredUsername/p/new", + component: () => import("@/views/Posts/Edit.vue"), + props: true, + name: GroupsRouteName.POST_CREATE, + }, + { + path: "/p/:slug/edit", + component: () => import("@/views/Posts/Edit.vue"), + props: (route: Route) => ({ ...route.params, ...{ isUpdate: true } }), + name: GroupsRouteName.POST_EDIT, + }, + { + path: "/p/:slug", + component: () => import("@/views/Posts/Post.vue"), + props: true, + name: GroupsRouteName.POST, + }, + { + path: "/@:preferredUsername/p", + component: () => import("@/views/Posts/List.vue"), + props: true, + name: GroupsRouteName.POSTS, + }, ]; diff --git a/js/src/router/index.ts b/js/src/router/index.ts index 52e64663f..19091f961 100644 --- a/js/src/router/index.ts +++ b/js/src/router/index.ts @@ -11,7 +11,7 @@ import { authGuardIfNeeded } from "./guards/auth-guard"; import Search from "../views/Search.vue"; import { settingsRoutes } from "./settings"; import { groupsRoutes } from "./groups"; -import { conversationRoutes } from "./conversation"; +import { discussionRoutes } from "./discussion"; import { userRoutes } from "./user"; import RouteName from "./name"; @@ -46,7 +46,7 @@ const router = new Router({ ...settingsRoutes, ...actorRoutes, ...groupsRoutes, - ...conversationRoutes, + ...discussionRoutes, ...errorRoutes, { path: "/search/:searchTerm/:searchType?", diff --git a/js/src/router/name.ts b/js/src/router/name.ts index 72243e1f3..9bd60c3b2 100644 --- a/js/src/router/name.ts +++ b/js/src/router/name.ts @@ -3,7 +3,7 @@ import { ActorRouteName } from "./actor"; import { ErrorRouteName } from "./error"; import { SettingsRouteName } from "./settings"; import { GroupsRouteName } from "./groups"; -import { ConversationRouteName } from "./conversation"; +import { DiscussionRouteName } from "./discussion"; import { UserRouteName } from "./user"; enum GlobalRouteName { @@ -29,6 +29,6 @@ export default { ...ActorRouteName, ...SettingsRouteName, ...GroupsRouteName, - ...ConversationRouteName, + ...DiscussionRouteName, ...ErrorRouteName, }; diff --git a/js/src/types/actor/group.model.ts b/js/src/types/actor/group.model.ts index 6d3e986e8..3194396e0 100644 --- a/js/src/types/actor/group.model.ts +++ b/js/src/types/actor/group.model.ts @@ -3,8 +3,9 @@ import { Paginate } from "../paginate"; import { IResource } from "../resource"; import { ITodoList } from "../todos"; import { IEvent } from "../event.model"; -import { IConversation } from "../conversations"; +import { IDiscussion } from "../discussions"; import { IPerson } from "./person.model"; +import { IPost } from "../post.model"; export enum MemberRole { NOT_APPROVED = "NOT_APPROVED", @@ -20,7 +21,7 @@ export interface IGroup extends IActor { members: Paginate; resources: Paginate; todoLists: Paginate; - conversations: Paginate; + discussions: Paginate; organizedEvents: Paginate; } @@ -39,9 +40,11 @@ export class Group extends Actor implements IGroup { todoLists: Paginate = { elements: [], total: 0 }; - conversations: Paginate = { elements: [], total: 0 }; + discussions: Paginate = { elements: [], total: 0 }; - organizedEvents!: Paginate; + organizedEvents: Paginate = { elements: [], total: 0 }; + + posts: Paginate = { elements: [], total: 0 }; constructor(hash: IGroup | {} = {}) { super(hash); diff --git a/js/src/types/comment.model.ts b/js/src/types/comment.model.ts index c1fc9988c..6e94df524 100644 --- a/js/src/types/comment.model.ts +++ b/js/src/types/comment.model.ts @@ -12,9 +12,10 @@ export interface IComment { originComment?: IComment; replies: IComment[]; event?: IEvent; - updatedAt?: Date; - deletedAt?: Date; + updatedAt?: Date | string; + deletedAt?: Date | string; totalReplies: number; + insertedAt?: Date | string; } export class CommentModel implements IComment { @@ -38,9 +39,11 @@ export class CommentModel implements IComment { event?: IEvent = undefined; - updatedAt?: Date = undefined; + updatedAt?: Date | string = undefined; - deletedAt?: Date = undefined; + deletedAt?: Date | string = undefined; + + insertedAt?: Date | string = undefined; totalReplies = 0; @@ -58,6 +61,7 @@ export class CommentModel implements IComment { this.replies = hash.replies; this.updatedAt = hash.updatedAt; this.deletedAt = hash.deletedAt; + this.insertedAt = new Date(hash.insertedAt as string); this.totalReplies = hash.totalReplies; } } diff --git a/js/src/types/conversations.ts b/js/src/types/conversations.ts deleted file mode 100644 index a0873ee9b..000000000 --- a/js/src/types/conversations.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IActor, IPerson } from "@/types/actor"; -import { IComment } from "@/types/comment.model"; -import { Paginate } from "@/types/paginate"; - -export interface IConversation { - id: string; - title: string; - slug: string; - creator: IPerson; - actor: IActor; - lastComment: IComment; - comments: Paginate; -} diff --git a/js/src/types/discussions.ts b/js/src/types/discussions.ts new file mode 100644 index 000000000..202691610 --- /dev/null +++ b/js/src/types/discussions.ts @@ -0,0 +1,44 @@ +import { IActor, IPerson } from "@/types/actor"; +import { IComment, CommentModel } from "@/types/comment.model"; +import { Paginate } from "@/types/paginate"; + +export interface IDiscussion { + id?: string; + title: string; + slug?: string; + creator?: IPerson; + actor?: IActor; + lastComment?: IComment; + comments: Paginate; +} + +export class Discussion implements IDiscussion { + id?: string; + + title = ""; + + comments: Paginate = { total: 0, elements: [] }; + + slug?: string = undefined; + + creator?: IPerson = undefined; + + actor?: IActor = undefined; + + lastComment?: IComment = undefined; + + constructor(hash?: IDiscussion) { + if (!hash) return; + + this.id = hash.id; + this.title = hash.title; + this.comments = { + total: hash.comments.total, + elements: hash.comments.elements.map((comment: IComment) => new CommentModel(comment)), + }; + this.slug = hash.slug; + this.creator = hash.creator; + this.actor = hash.actor; + this.lastComment = hash.lastComment; + } +} diff --git a/js/src/types/post.model.ts b/js/src/types/post.model.ts new file mode 100644 index 000000000..a49d14284 --- /dev/null +++ b/js/src/types/post.model.ts @@ -0,0 +1,26 @@ +import { ITag } from "./tag.model"; +import { IPicture } from "./picture.model"; +import { IActor } from "./actor"; + +export enum PostVisibility { + PUBLIC = "PUBLIC", + UNLISTED = "UNLISTED", + RESTRICTED = "RESTRICTED", + PRIVATE = "PRIVATE", +} + +export interface IPost { + id?: string; + slug?: string; + url?: string; + local: boolean; + title: string; + body: string; + tags?: ITag[]; + picture?: IPicture | null; + draft: boolean; + visibility: PostVisibility; + author?: IActor; + attributedTo?: IActor; + publishAt?: Date; +} diff --git a/js/src/utils/i18n.ts b/js/src/utils/i18n.ts index 3baec68e0..a19c3ab63 100644 --- a/js/src/utils/i18n.ts +++ b/js/src/utils/i18n.ts @@ -4,7 +4,7 @@ import messages from "../i18n/index"; let language = document.documentElement.getAttribute("lang") as string; language = language || ((window.navigator as any).userLanguage || window.navigator.language).replace(/-/, "_"); -const locale = language && messages.hasOwnProperty(language) ? language : language.split("-")[0]; +export const locale = language && messages.hasOwnProperty(language) ? language : language.split("-")[0]; Vue.use(VueI18n); diff --git a/js/src/views/About/AboutInstance.vue b/js/src/views/About/AboutInstance.vue index 84c15b74f..755a0aca8 100644 --- a/js/src/views/About/AboutInstance.vue +++ b/js/src/views/About/AboutInstance.vue @@ -83,6 +83,7 @@ import { IStatistics } from "../../types/statistics.model"; }) export default class AboutInstance extends Vue { config!: IConfig; + statistics!: IStatistics; get isContactEmail(): boolean { @@ -97,7 +98,8 @@ export default class AboutInstance extends Vue { if (!this.config.contact) return null; if (this.isContactEmail) { return { uri: `mailto:${this.config.contact}`, text: this.config.contact }; - } else if (this.isContactURL) { + } + if (this.isContactURL) { return { uri: this.config.contact, text: this.urlToHostname(this.config.contact) || (this.$t("Contact") as string), diff --git a/js/src/views/Admin/AdminProfile.vue b/js/src/views/Admin/AdminProfile.vue index ae418b913..8683a27a5 100644 --- a/js/src/views/Admin/AdminProfile.vue +++ b/js/src/views/Admin/AdminProfile.vue @@ -160,7 +160,7 @@ const EVENTS_PER_PAGE = 10; }, }) export default class AdminProfile extends Vue { - @Prop({ required: true }) id!: String; + @Prop({ required: true }) id!: string; person!: IPerson; @@ -171,6 +171,7 @@ export default class AdminProfile extends Vue { EVENTS_PER_PAGE = EVENTS_PER_PAGE; organizedEventsPage = 1; + participationsPage = 1; get metadata(): Array { diff --git a/js/src/views/Admin/AdminUserProfile.vue b/js/src/views/Admin/AdminUserProfile.vue index 4afe2eac5..048e7f090 100644 --- a/js/src/views/Admin/AdminUserProfile.vue +++ b/js/src/views/Admin/AdminUserProfile.vue @@ -81,7 +81,7 @@ import { IPerson } from "../../types/actor"; }, }) export default class AdminUserProfile extends Vue { - @Prop({ required: true }) id!: String; + @Prop({ required: true }) id!: string; user!: IUser; diff --git a/js/src/views/Admin/Profiles.vue b/js/src/views/Admin/Profiles.vue index 59e16b05b..083ba156d 100644 --- a/js/src/views/Admin/Profiles.vue +++ b/js/src/views/Admin/Profiles.vue @@ -105,13 +105,19 @@ const PROFILES_PER_PAGE = 10; }) export default class Profiles extends Vue { page = 1; + preferredUsername = ""; + name = ""; + domain = ""; + local = true; + suspended = false; PROFILES_PER_PAGE = PROFILES_PER_PAGE; + RouteName = RouteName; async onPageChange(page: number) { diff --git a/js/src/views/Admin/Settings.vue b/js/src/views/Admin/Settings.vue index 1aed29a3b..923782bb7 100644 --- a/js/src/views/Admin/Settings.vue +++ b/js/src/views/Admin/Settings.vue @@ -270,6 +270,7 @@ export default class Settings extends Vue { adminSettings!: IAdminSettings; InstanceTermsType = InstanceTermsType; + InstancePrivacyType = InstancePrivacyType; RouteName = RouteName; diff --git a/js/src/views/Admin/Users.vue b/js/src/views/Admin/Users.vue index fa1c24c2f..2f0100f4a 100644 --- a/js/src/views/Admin/Users.vue +++ b/js/src/views/Admin/Users.vue @@ -109,9 +109,11 @@ const USERS_PER_PAGE = 10; }) export default class Users extends Vue { page = 1; + email = ""; USERS_PER_PAGE = USERS_PER_PAGE; + RouteName = RouteName; async onPageChange(page: number) { diff --git a/js/src/views/Conversations/Conversation.vue b/js/src/views/Conversations/Conversation.vue deleted file mode 100644 index 220cb5b10..000000000 --- a/js/src/views/Conversations/Conversation.vue +++ /dev/null @@ -1,243 +0,0 @@ - - - diff --git a/js/src/views/Conversations/Create.vue b/js/src/views/Discussions/Create.vue similarity index 62% rename from js/src/views/Conversations/Create.vue rename to js/src/views/Discussions/Create.vue index e16cdea19..e79ce3ade 100644 --- a/js/src/views/Conversations/Create.vue +++ b/js/src/views/Discussions/Create.vue @@ -2,13 +2,13 @@

{{ $t("Create a discussion") }}

-
+ - + - + @@ -20,7 +20,7 @@ import { Component, Prop, Vue } from "vue-property-decorator"; import { IGroup, IPerson } from "@/types/actor"; import { CURRENT_ACTOR_CLIENT, FETCH_GROUP } from "@/graphql/actor"; -import { CREATE_CONVERSATION } from "@/graphql/conversation"; +import { CREATE_DISCUSSION } from "@/graphql/discussion"; import RouteName from "../../router/name"; @Component({ @@ -41,36 +41,45 @@ import RouteName from "../../router/name"; }, }, }, + metaInfo() { + return { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + title: this.$t("Create a discussion") as string, + // all titles will be injected into this template + titleTemplate: "%s | Mobilizon", + }; + }, }) -export default class CreateConversation extends Vue { +export default class CreateDiscussion extends Vue { @Prop({ type: String, required: true }) preferredUsername!: string; group!: IGroup; currentActor!: IPerson; - conversation = { title: "", text: "" }; + discussion = { title: "", text: "" }; - async createConversation() { + async createDiscussion() { try { const { data } = await this.$apollo.mutate({ - mutation: CREATE_CONVERSATION, + mutation: CREATE_DISCUSSION, variables: { - title: this.conversation.title, - text: this.conversation.text, + title: this.discussion.title, + text: this.discussion.text, actorId: this.group.id, creatorId: this.currentActor.id, }, - // update: (store, { data: { createConversation } }) => { + // update: (store, { data: { createDiscussion } }) => { // // TODO: update group list cache // }, }); await this.$router.push({ - name: RouteName.CONVERSATION, + name: RouteName.DISCUSSION, params: { - id: data.createConversation.id, - slug: data.createConversation.slug, + id: data.createDiscussion.id, + slug: data.createDiscussion.slug, }, }); } catch (err) { diff --git a/js/src/views/Discussions/Discussion.vue b/js/src/views/Discussions/Discussion.vue new file mode 100644 index 000000000..063012f8d --- /dev/null +++ b/js/src/views/Discussions/Discussion.vue @@ -0,0 +1,350 @@ + + + diff --git a/js/src/views/Conversations/ConversationsList.vue b/js/src/views/Discussions/DiscussionsList.vue similarity index 70% rename from js/src/views/Conversations/ConversationsList.vue rename to js/src/views/Discussions/DiscussionsList.vue index 977c32022..8fc9790e8 100644 --- a/js/src/views/Conversations/ConversationsList.vue +++ b/js/src/views/Discussions/DiscussionsList.vue @@ -17,7 +17,7 @@
  • {{ $t("Discussions") }}
    -
    - +
    {{ $t("New discussion") }} { const res = participations.filter( ({ event, role }) => event.beginsOn != null && role !== ParticipantRole.REJECTED diff --git a/js/src/views/Event/Participants.vue b/js/src/views/Event/Participants.vue index ab1932e27..c68357a6b 100644 --- a/js/src/views/Event/Participants.vue +++ b/js/src/views/Event/Participants.vue @@ -91,7 +91,7 @@ {{ props.row.actor.name }}
    @{{ props.row.actor.preferredUsername }}@{{ usernameWithDomain(props.row.actor) }} @@ -184,6 +184,7 @@ diff --git a/js/src/views/Posts/List.vue b/js/src/views/Posts/List.vue new file mode 100644 index 000000000..e42da9f92 --- /dev/null +++ b/js/src/views/Posts/List.vue @@ -0,0 +1,86 @@ + + + diff --git a/js/src/views/Posts/Post.vue b/js/src/views/Posts/Post.vue new file mode 100644 index 000000000..1989e78ab --- /dev/null +++ b/js/src/views/Posts/Post.vue @@ -0,0 +1,184 @@ + + + + diff --git a/js/src/views/Resources/ResourceFolder.vue b/js/src/views/Resources/ResourceFolder.vue index 28888591e..d3f66b575 100644 --- a/js/src/views/Resources/ResourceFolder.vue +++ b/js/src/views/Resources/ResourceFolder.vue @@ -118,7 +118,7 @@ -
    +

    {{ $t("No resources in this folder") }}

    @@ -470,12 +470,12 @@ export default class Resources extends Mixins(ResourceMixin) { handleRename(resource: IResource) { this.renameModal = true; - this.updatedResource = Object.assign({}, resource); + this.updatedResource = { ...resource }; } handleMove(resource: IResource) { this.moveModal = true; - this.updatedResource = Object.assign({}, resource); + this.updatedResource = { ...resource }; } async moveResource(resource: IResource, oldParent: IResource | undefined) { diff --git a/js/yarn.lock b/js/yarn.lock index fb896df7a..ce0a49a75 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -49,32 +49,32 @@ "@babel/highlight" "^7.10.4" "@babel/compat-data@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.4.tgz#706a6484ee6f910b719b696a9194f8da7d7ac241" - integrity sha512-t+rjExOrSVvjQQXNp5zAIYDp00KjdvGl/TpDX5REPr0S9IAIPQMTilcfG6q8c0QFmj9lSTVySV2VTsyggvtNIw== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.5.tgz#d38425e67ea96b1480a3f50404d1bf85676301a6" + integrity sha512-mPVoWNzIpYJHbWje0if7Ck36bpbtTvIxOi9+6WSK9wjGEXearAqlwBoTQvVjsAY2VIwgcs8V940geY3okzRCEw== dependencies: browserslist "^4.12.0" invariant "^2.2.4" semver "^5.5.0" "@babel/core@^7.7.5", "@babel/core@^7.9.6": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.4.tgz#780e8b83e496152f8dd7df63892b2e052bf1d51d" - integrity sha512-3A0tS0HWpy4XujGc7QtOIHTeNwUgWaZc/WuS5YQrfhU67jnVmsD6OGPc1AKHH0LJHQICGncy3+YUjIhVlfDdcA== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330" + integrity sha512-O34LQooYVDXPl7QWCdW9p4NR+QlzOr7xShPPJz8GsuCU3/8ua/wqTr7gmnxXv+WBESiGU/G5s16i6tUvHkNb+w== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.4" + "@babel/generator" "^7.10.5" + "@babel/helper-module-transforms" "^7.10.5" "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.10.4" + "@babel/parser" "^7.10.5" "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/traverse" "^7.10.5" + "@babel/types" "^7.10.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" json5 "^2.1.2" - lodash "^4.17.13" + lodash "^4.17.19" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" @@ -90,14 +90,13 @@ source-map "^0.5.0" trim-right "^1.0.1" -"@babel/generator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.4.tgz#e49eeed9fe114b62fa5b181856a43a5e32f5f243" - integrity sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng== +"@babel/generator@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" + integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.10.5" jsesc "^2.5.1" - lodash "^4.17.13" source-map "^0.5.0" "@babel/helper-annotate-as-pure@^7.10.4": @@ -126,13 +125,13 @@ levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-create-class-features-plugin@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.4.tgz#2d4015d0136bd314103a70d84a7183e4b344a355" - integrity sha512-9raUiOsXPxzzLjCXeosApJItoMnX3uyT4QdM2UldffuGApNrF8e938MwNpDCK9CPoyxrEoCgT+hObJc3mZa6lQ== +"@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== dependencies: "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" "@babel/helper-optimise-call-expression" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" @@ -148,13 +147,13 @@ regexpu-core "^4.7.0" "@babel/helper-define-map@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.4.tgz#f037ad794264f729eda1889f4ee210b870999092" - integrity sha512-nIij0oKErfCnLUCWaCaHW0Bmtl2RO9cN7+u2QT8yqTywgALKlyUVOvHDElh+b5DwVC6YB1FOYFOTWcN/+41EDA== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" + integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== dependencies: "@babel/helper-function-name" "^7.10.4" - "@babel/types" "^7.10.4" - lodash "^4.17.13" + "@babel/types" "^7.10.5" + lodash "^4.17.19" "@babel/helper-explode-assignable-expression@^7.10.4": version "7.10.4" @@ -187,12 +186,12 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-member-expression-to-functions@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.4.tgz#7cd04b57dfcf82fce9aeae7d4e4452fa31b8c7c4" - integrity sha512-m5j85pK/KZhuSdM/8cHUABQTAslV47OjfIB9Cc7P+PvlAoBzdb79BGNfw8RhT5Mq3p+xGd0ZfAKixbrUZx0C7A== +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee" + integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.10.5" "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.8.3": version "7.10.4" @@ -201,18 +200,18 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz#ca1f01fdb84e48c24d7506bb818c961f1da8805d" - integrity sha512-Er2FQX0oa3nV7eM1o0tNCTx7izmQtwAQsIiaLRWtavAAEcskb0XJ5OjJbVrYXWOTr8om921Scabn4/tzlx7j1Q== +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6" + integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA== dependencies: "@babel/helper-module-imports" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-simple-access" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - lodash "^4.17.13" + "@babel/types" "^7.10.5" + lodash "^4.17.19" "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" @@ -227,11 +226,11 @@ integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== "@babel/helper-regex@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.4.tgz#59b373daaf3458e5747dece71bbaf45f9676af6d" - integrity sha512-inWpnHGgtg5NOF0eyHlC0/74/VkdRITY9dtTpB2PrxKKn+AkVMRiZz/Adrx+Ssg+MLDesi2zohBW6MVq6b4pOQ== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" + integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== dependencies: - lodash "^4.17.13" + lodash "^4.17.19" "@babel/helper-remap-async-to-generator@^7.10.4": version "7.10.4" @@ -302,15 +301,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.10.4", "@babel/parser@^7.6.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.4.tgz#9eedf27e1998d87739fb5028a5120557c06a1a64" - integrity sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA== +"@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" + integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== "@babel/plugin-proposal-async-generator-functions@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.4.tgz#4b65abb3d9bacc6c657aaa413e56696f9f170fc6" - integrity sha512-MJbxGSmejEFVOANAezdO39SObkURO5o/8b6fSH6D1pi9RZQt+ldppKPXfqgUWpSQ9asM6xaSaSJIaeWMDRP0Zg== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" + integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-remap-async-to-generator" "^7.10.4" @@ -325,11 +324,11 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-proposal-decorators@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.4.tgz#fe20ef10cc73f386f70910fca48798041cd357c7" - integrity sha512-JHTWjQngOPv+ZQQqOGv2x6sCCr4IYWy7S1/VH6BE9ZfkoLrdQ2GpEP3tfb5M++G9PwvqjhY8VC/C3tXm+/eHvA== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4" + integrity sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-create-class-features-plugin" "^7.10.5" "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-decorators" "^7.10.4" @@ -514,12 +513,11 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-block-scoping@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.4.tgz#a670d1364bb5019a621b9ea2001482876d734787" - integrity sha512-J3b5CluMg3hPUii2onJDRiaVbPtKFPLEaV5dOPY5OeAbDi1iU/UbbFFTgwb7WnanaDy7bjU35kc26W3eM5Qa0A== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.5.tgz#b81b8aafefbfe68f0f65f7ef397b9ece68a6037d" + integrity sha512-6Ycw3hjpQti0qssQcA6AMSFDHeNJ++R6dIMnpRqUjFeBBTmTDPa8zgF90OVfTvAo11mXZTlVUViY1g8ffrURLg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - lodash "^4.17.13" "@babel/plugin-transform-classes@^7.10.4": version "7.10.4" @@ -602,11 +600,11 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-modules-amd@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.4.tgz#cb407c68b862e4c1d13a2fc738c7ec5ed75fc520" - integrity sha512-3Fw+H3WLUrTlzi3zMiZWp3AR4xadAEMv6XRCYnd5jAlLM61Rn+CRJaZMaNvIpcJpQ3vs1kyifYvEVPFfoSkKOA== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" + integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== dependencies: - "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.5" "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" @@ -621,12 +619,12 @@ babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-systemjs@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.4.tgz#8f576afd943ac2f789b35ded0a6312f929c633f9" - integrity sha512-Tb28LlfxrTiOTGtZFsvkjpyjCl9IoaRI52AEU/VIwOwvDQWtbNJsAqTXzh+5R7i74e/OZHH2c2w2fsOqAfnQYQ== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" + integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== dependencies: "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.5" "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" @@ -661,9 +659,9 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/plugin-transform-parameters@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.4.tgz#7b4d137c87ea7adc2a0f3ebf53266871daa6fced" - integrity sha512-RurVtZ/D5nYfEg0iVERXYKEgDFeesHrHfx8RT05Sq57ucj2eOYAP6eu5fynL4Adju4I/mP/I6SO0DqNWAXjfLQ== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" + integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== dependencies: "@babel/helper-get-function-arity" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" @@ -690,9 +688,9 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-runtime@^7.9.6": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.4.tgz#594fb53453ea1b6f0779cceb48ce0718a447feb7" - integrity sha512-8ULlGv8p+Vuxu+kz2Y1dk6MYS2b/Dki+NO6/0ZlfSj5tMalfDL7jI/o/2a+rrWLqSXvnadEqc2WguB4gdQIxZw== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.5.tgz#3b39b7b24830e0c2d8ff7a4489fe5cf99fbace86" + integrity sha512-tV4V/FjElJ9lQtyjr5xD2IFFbgY46r7EeVu5a8CpEKT5laheHKSlFeHjpkPppW3PqzGLAuv5k2qZX5LgVZIX5w== dependencies: "@babel/helper-module-imports" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" @@ -722,9 +720,9 @@ "@babel/helper-regex" "^7.10.4" "@babel/plugin-transform-template-literals@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.4.tgz#e6375407b30fcb7fcfdbba3bb98ef3e9d36df7bc" - integrity sha512-4NErciJkAYe+xI5cqfS8pV/0ntlY5N5Ske/4ImxAVX7mk9Rxt2bwDTGv1Msc2BRJvWQcmYEC+yoMLdX22aE4VQ== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" + integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" @@ -833,21 +831,13 @@ esutils "^2.0.2" "@babel/runtime-corejs2@^7.0.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.10.4.tgz#5d48ee239624d511c88208da86c27a161ee01cf7" - integrity sha512-9sArmpZDQsnR1yyAcU51DxQrntWxt0LUKjPp3pIyo7kVLfaqKt8muppcT87QmFkXV5H50qXAF8JWOjk0jaXRYA== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.10.5.tgz#8daa1ceccc0468e5c2e15f124e3f51c2b3033b49" + integrity sha512-LJwyb1ac//Jr2zrGTTaNJhrP1wYCgVw9rzHbQPogKXCTLQ60EEWgeNtuqs6cLsq64O557SYzziCrOxNp0rRi8w== dependencies: core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/runtime-corejs3@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz#f29fc1990307c4c57b10dbd6ce667b27159d9e0d" - integrity sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw== - dependencies: - core-js-pure "^3.0.0" - regenerator-runtime "^0.13.4" - "@babel/runtime@7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f" @@ -856,9 +846,9 @@ regenerator-runtime "^0.12.0" "@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.6": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99" - integrity sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw== + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" + integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== dependencies: regenerator-runtime "^0.13.4" @@ -871,20 +861,20 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.4.tgz#e642e5395a3b09cc95c8e74a27432b484b697818" - integrity sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q== +"@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" + integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.4" + "@babel/generator" "^7.10.5" "@babel/helper-function-name" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/parser" "^7.10.5" + "@babel/types" "^7.10.5" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.13" + lodash "^4.17.19" "@babel/types@7.0.0-beta.38": version "7.0.0-beta.38" @@ -895,13 +885,13 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@babel/types@^7.10.4", "@babel/types@^7.4.4", "@babel/types@^7.6.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.4.tgz#369517188352e18219981efd156bfdb199fff1ee" - integrity sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg== +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.4.4", "@babel/types@^7.6.0", "@babel/types@^7.6.1", "@babel/types@^7.9.6": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" + integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== dependencies: "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.13" + lodash "^4.17.19" to-fast-properties "^2.0.0" "@cypress/listr-verbose-renderer@0.4.1": @@ -1008,9 +998,9 @@ yargs "^8.0.2" "@mdi/font@^5.0.45": - version "5.3.45" - resolved "https://registry.yarnpkg.com/@mdi/font/-/font-5.3.45.tgz#086d3ef77dee260c04dd5a593af602c250e5b315" - integrity sha512-SD5d2vHEKRvDCInZQFXOwiFpBlzpuZOiqwxKf6E+zCt7UDc52TUSrL0+TXqY57VQh/SnTpZVXM+Uvs21OdPFWg== + version "5.4.55" + resolved "https://registry.yarnpkg.com/@mdi/font/-/font-5.4.55.tgz#f34263882251ac23f37c1312988e1f10256dc74c" + integrity sha512-M+Wdcs4nZ4/Kid949fcI0DsnvHtpE6pwk6Hv8YJZDp+Zne7ZtYdIN0z73cvcANkbyNnY3ncScULGMIceNd0xxQ== "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -1046,7 +1036,7 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@popperjs/core@^2.3.2": +"@popperjs/core@^2.4.4": version "2.4.4" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398" integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg== @@ -1082,22 +1072,10 @@ resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== -"@types/babel-types@*", "@types/babel-types@^7.0.0": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3" - integrity sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ== - -"@types/babylon@^6.16.2": - version "6.16.5" - resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.5.tgz#1c5641db69eb8cdf378edd25b4be7754beeb48b4" - integrity sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w== - dependencies: - "@types/babel-types" "*" - "@types/chai@^4.2.11": - version "4.2.11" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" - integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + version "4.2.12" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.12.tgz#6160ae454cd89dae05adc3bb97997f488b608201" + integrity sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ== "@types/color-name@^1.1.1": version "1.1.1" @@ -1115,9 +1093,9 @@ integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== "@types/glob@^7.1.1": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987" - integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA== + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== dependencies: "@types/minimatch" "*" "@types/node" "*" @@ -1145,16 +1123,16 @@ "@types/leaflet" "*" "@types/leaflet@*", "@types/leaflet@^1.5.2": - version "1.5.13" - resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.13.tgz#e0e41612b236a6a37877a69a6eb996d2a9ba6905" - integrity sha512-aCNOIeoukY8DIQUs/8bNiiKQKc75HSFwo1YqcFaLe+SkG4DL+0ygKCDfhfZ54UX8k28pn5uA3QJEAw3wa2hqHw== + version "1.5.17" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.17.tgz#b2153dc12c344e6896a93ffc6b61ac79da251e5b" + integrity sha512-2XYq9k6kNjhNI7PaTz8Rdxcc8Vzwu97OaS9CtcrTxnTSxFUGwjlGjTDvhTLJU+JRSfZ4lBwGcl0SjZHALdVr6g== dependencies: "@types/geojson" "*" "@types/lodash@^4.14.141": - version "4.14.157" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" - integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ== + version "4.14.158" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.158.tgz#b38ea8b6fe799acd076d7a8d7ab71c26ef77f785" + integrity sha512-InCEXJNTv/59yO4VSfuvNrZHt7eeNtWQEgnieIA+mIC+MOWM9arOWG2eQ8Vhk6NbOre6/BidiXhkZYeDY9U35w== "@types/minimatch@*": version "3.0.3" @@ -1172,9 +1150,9 @@ integrity sha512-6nlq2eEh75JegDGUXis9wGTYIJpUvbori4qx++PRKQsV3YRkaqUNPNykzphniqPSZADXCouBuAnyptjUkMkhvw== "@types/node@*", "@types/node@>=6": - version "14.0.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" - integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== + version "14.0.27" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1" + integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1247,9 +1225,9 @@ integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== "@types/uglify-js@*": - version "3.9.2" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.2.tgz#01992579debba674e1e359cd6bcb1a1d0ab2e02b" - integrity sha512-d6dIfpPbF+8B7WiCi2ELY7m0w1joD8cRW4ms88Emdb2w062NeEpbNCeWwVCgzLRpVG+5e74VFSg4rgJ2xXjEiQ== + version "3.9.3" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b" + integrity sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w== dependencies: source-map "^0.6.1" @@ -1271,18 +1249,18 @@ integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== "@types/webpack-sources@*": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.0.tgz#e58f1f05f87d39a5c64cf85705bdbdbb94d4d57e" - integrity sha512-c88dKrpSle9BtTqR6ifdaxu1Lvjsl3C5OsfvuUbUwdXymshv1TkufUAXBajCCUM/f/TmnkZC/Esb03MinzSiXQ== + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.2.tgz#5d3d4dea04008a779a90135ff96fb5c0c9e6292c" + integrity sha512-77T++JyKow4BQB/m9O96n9d/UUHWLQHlcqXb9Vsf4F1+wKNrrlWNFPDLKNT92RJnCSL6CieTc+NDXtCVZswdTw== dependencies: "@types/node" "*" "@types/source-list-map" "*" source-map "^0.7.3" "@types/webpack@^4.4.31": - version "4.41.18" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.18.tgz#2945202617866ecdffa582087f1b6de04a7eed55" - integrity sha512-mQm2R8vV2BZE/qIDVYqmBVLfX73a8muwjs74SpjEyJWJxeXBbsI9L65Pcia9XfYLYWzD1c1V8m+L0p30y2N7MA== + version "4.41.21" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.21.tgz#cc685b332c33f153bb2f5fc1fa3ac8adeb592dee" + integrity sha512-2j9WVnNrr/8PLAB5csW44xzQSJwS26aOnICsP3pSGCEdsu6KYtfQ6QJsVUKHWRnm1bL7HziJsfh5fHqth87yKA== dependencies: "@types/anymatch" "*" "@types/node" "*" @@ -1316,14 +1294,14 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.5.0.tgz#d09f9ffb890d1b15a7ffa9975fae92eee05597c4" - integrity sha512-zGNOrVi5Wz0jcjUnFZ6QUD0MCox5hBuVwemGCew2qJzUX5xPoyR+0EzS5qD5qQXL/vnQ8Eu+nv03tpeFRwLrDg== +"@typescript-eslint/experimental-utils@3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.7.1.tgz#ab036caaed4c870d22531d41f9352f3147364d61" + integrity sha512-TqE97pv7HrqWcGJbLbZt1v59tcqsSVpWTOf1AqrWK7n8nok2sGgVtYRuGXeNeLw3wXlLEbY1MKP3saB2HsO/Ng== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.5.0" - "@typescript-eslint/typescript-estree" "3.5.0" + "@typescript-eslint/types" "3.7.1" + "@typescript-eslint/typescript-estree" "3.7.1" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1338,20 +1316,20 @@ eslint-visitor-keys "^1.1.0" "@typescript-eslint/parser@^3.0.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.5.0.tgz#9ff8c11877c48df24e10e19d7bf542ee0359500d" - integrity sha512-sU07VbYB70WZHtgOjH/qfAp1+OwaWgrvD1Km1VXqRpcVxt971PMTU7gJtlrCje0M+Sdz7xKAbtiyIu+Y6QdnVA== + version "3.7.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.7.1.tgz#5d9ccecb116d12d9c6073e9861c57c9b1aa88128" + integrity sha512-W4QV/gXvfIsccN8225784LNOorcm7ch68Fi3V4Wg7gmkWSQRKevO4RrRqWo6N/Z/myK1QAiGgeaXN57m+R/8iQ== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.5.0" - "@typescript-eslint/types" "3.5.0" - "@typescript-eslint/typescript-estree" "3.5.0" + "@typescript-eslint/experimental-utils" "3.7.1" + "@typescript-eslint/types" "3.7.1" + "@typescript-eslint/typescript-estree" "3.7.1" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/types@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.5.0.tgz#4e3d2a2272268d8ec3e3e4a37152a64956682639" - integrity sha512-Dreqb5idi66VVs1QkbAwVeDmdJG+sDtofJtKwKCZXIaBsINuCN7Jv5eDIHrS0hFMMiOvPH9UuOs4splW0iZe4Q== +"@typescript-eslint/types@3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.7.1.tgz#90375606b2fd73c1224fe9e397ee151e28fa1e0c" + integrity sha512-PZe8twm5Z4b61jt7GAQDor6KiMhgPgf4XmUb9zdrwTbgtC/Sj29gXP1dws9yEn4+aJeyXrjsD9XN7AWFhmnUfg== "@typescript-eslint/typescript-estree@2.34.0": version "2.34.0" @@ -1366,13 +1344,13 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.5.0.tgz#dfc895db21a381b84f24c2a719f5bf9c600dcfdc" - integrity sha512-Na71ezI6QP5WVR4EHxwcBJgYiD+Sre9BZO5iJK2QhrmRPo/42+b0no/HZIrdD1sjghzlYv7t+7Jis05M1uMxQg== +"@typescript-eslint/typescript-estree@3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.7.1.tgz#ce1ffbd0fa53f34d4ce851a7a364e392432f6eb3" + integrity sha512-m97vNZkI08dunYOr2lVZOHoyfpqRs0KDpd6qkGaIcLGhQ2WPtgHOd/eVbsJZ0VYCQvupKrObAGTOvk3tfpybYA== dependencies: - "@typescript-eslint/types" "3.5.0" - "@typescript-eslint/visitor-keys" "3.5.0" + "@typescript-eslint/types" "3.7.1" + "@typescript-eslint/visitor-keys" "3.7.1" debug "^4.1.1" glob "^7.1.6" is-glob "^4.0.1" @@ -1380,10 +1358,10 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.5.0.tgz#73c1ea2582f814735e4afdc1cf6f5e3af78db60a" - integrity sha512-7cTp9rcX2sz9Z+zua9MCOX4cqp5rYyFD5o8LlbSpXrMTXoRdngTtotRZEkm8+FNMHPWYFhitFK+qt/brK8BVJQ== +"@typescript-eslint/visitor-keys@3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.7.1.tgz#b90191e74efdee656be8c5a30f428ed16dda46d1" + integrity sha512-xn22sQbEya+Utj2IqJHGLA3i1jDzR43RzWupxojbSWnj3nnPLavaQmWe5utw03CwYao3r00qzXfgJMGNkrzrAA== dependencies: eslint-visitor-keys "^1.1.0" @@ -1636,9 +1614,9 @@ strip-ansi "^6.0.0" "@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.2.tgz#8213a5ff3202f9f2137fe55370f9e8b9656081c3" - integrity sha512-QLq9z8m79mCinpaEeSURhnNCN6djxpHw0lpP/bodMlt5kALfONpryMthvnrQOlTcIKoF+VoPi+lPHUYeDFPXug== + version "3.2.0" + resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz#8f85182ceed28e9b3c75313de669f83166d11e5d" + integrity sha512-lejBLa7xAMsfiZfNp7Kv51zOzifnb29FwdnMLa96z26kXErPFioSf9BMcePVIQ6/Gc6/mC0UrPpxAWIHyae0vw== dependencies: consolidate "^0.15.1" hash-sum "^1.0.2" @@ -1676,9 +1654,9 @@ vue-eslint-parser "^7.0.0" "@vue/preload-webpack-plugin@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.1.tgz#18723530d304f443021da2292d6ec9502826104a" - integrity sha512-8VCoJeeH8tCkzhkpfOkt+abALQkS11OIHhte5MBzYaKMTqK0A3ZAKEUVAffsOklhEv7t0yrQt696Opnu9oAx+w== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz#ceb924b4ecb3b9c43871c7a429a02f8423e621ab" + integrity sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ== "@vue/test-utils@1.0.3": version "1.0.3" @@ -1894,13 +1872,6 @@ acorn-dynamic-import@^4.0.0: resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== -acorn-globals@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" - integrity sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8= - dependencies: - acorn "^4.0.4" - acorn-globals@^4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" @@ -1924,16 +1895,6 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn@^3.1.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" - integrity sha1-ReN/s56No/JbruP/U2niu18iAXo= - -acorn@^4.0.4, acorn@~4.0.2: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= - acorn@^6.0.1, acorn@^6.1.1, acorn@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" @@ -1975,9 +1936,9 @@ ajv-errors@^1.0.0: integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.0.tgz#5c894537098785926d71e696114a53ce768ed773" - integrity sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw== + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== ajv@5, ajv@^5.5.1: version "5.5.2" @@ -1989,25 +1950,16 @@ ajv@5, ajv@^5.5.1: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5: - version "6.12.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" - integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3: + version "6.12.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -2463,6 +2415,11 @@ asn1@~0.2.3: dependencies: safer-buffer "~2.1.0" +assert-never@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe" + integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw== + assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" @@ -2486,12 +2443,7 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -ast-types@0.12.4, ast-types@^0.12.2: - version "0.12.4" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.12.4.tgz#71ce6383800f24efc9a1a3308f3a6e420a0974d1" - integrity sha512-ky/YVYCbtVAS8TdMIaTiPFHwEpRB5z1hctepJplTr3UW5q8TDrpIMCILyk8pmLxGtn2KCtC/lSn7zOsaI7nzDw== - -ast-types@0.13.3, ast-types@^0.13.2, ast-types@~0.13.2: +ast-types@0.13.3, ast-types@^0.13.2, ast-types@^0.13.3, ast-types@~0.13.2: version "0.13.3" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.3.tgz#50da3f28d17bdbc7969a3a2d83a0e4a72ae755a7" integrity sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA== @@ -2546,12 +2498,12 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^9.8.0: - version "9.8.4" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.4.tgz#736f1012673a70fa3464671d78d41abd54512863" - integrity sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A== + version "9.8.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa" + integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg== dependencies: browserslist "^4.12.0" - caniuse-lite "^1.0.30001087" + caniuse-lite "^1.0.30001097" colorette "^1.2.0" normalize-range "^0.1.2" num2fraction "^1.2.2" @@ -2632,15 +2584,12 @@ babel-runtime@6.26.0, babel-runtime@^6.25.0, babel-runtime@^6.26.0: core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= +babel-walk@3.0.0-canary-5: + version "3.0.0-canary-5" + resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11" + integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw== dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" + "@babel/types" "^7.9.6" babylon@^6.18.0: version "6.18.0" @@ -2942,12 +2891,12 @@ browserslist@4.7.0: node-releases "^1.1.29" browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5: - version "4.12.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.2.tgz#76653d7e4c57caa8a1a28513e2f4e197dc11a711" - integrity sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw== + version "4.13.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" + integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== dependencies: - caniuse-lite "^1.0.30001088" - electron-to-chromium "^1.3.483" + caniuse-lite "^1.0.30001093" + electron-to-chromium "^1.3.488" escalade "^3.0.1" node-releases "^1.1.58" @@ -3178,11 +3127,6 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= - camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -3208,10 +3152,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001087, caniuse-lite@^1.0.30001088: - version "1.0.30001093" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001093.tgz#833e80f64b1a0455cbceed2a4a3baf19e4abd312" - integrity sha512-0+ODNoOjtWD5eS9aaIpf4K0gQqZfILNY4WSNuYzeT1sXni+lMrrVjc0odEobJt6wrODofDZUX8XYi/5y7+xl8g== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001097: + version "1.0.30001109" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001109.tgz#a9f3f26a0c3753b063d7acbb48dfb9c0e46f2b19" + integrity sha512-4JIXRodHzdS3HdK8nSgIqXYLExOvG+D2/EenSvcub2Kp3QEADjo2v2oUn5g0n0D+UNwG9BtwKOyGcSq2qvQXvQ== capture-stack-trace@^1.0.0: version "1.0.1" @@ -3233,14 +3177,6 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - chai@^4.1.2: version "4.2.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" @@ -3337,7 +3273,7 @@ character-entities@^1.0.0: resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== -character-parser@^2.1.1: +character-parser@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A= @@ -3388,10 +3324,10 @@ chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.3.0, chokidar@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" - integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== +chokidar@^3.3.0, chokidar@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1" + integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -3453,7 +3389,7 @@ classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== -clean-css@4.2.x, clean-css@^4.1.11: +clean-css@4.2.x: version "4.2.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== @@ -3530,9 +3466,9 @@ cli-spinners@^0.1.2: integrity sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw= cli-spinners@^2.0.0, cli-spinners@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.3.0.tgz#0632239a4b5aa4c958610142c34bb7a651fc8df5" - integrity sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w== + version "2.4.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" + integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== cli-truncate@^0.2.1: version "0.2.1" @@ -3575,15 +3511,6 @@ clipboardy@^2.3.0: execa "^1.0.0" is-wsl "^2.1.1" -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -3671,9 +3598,9 @@ code-point-at@^1.0.0: integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= codemirror@^5.39.0: - version "5.55.0" - resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.55.0.tgz#23731f641288f202a6858fdc878f3149e0e04363" - integrity sha512-TumikSANlwiGkdF/Blnu/rqovZ0Y3Jh8yy9TqrPbSM0xxSucq3RgnpVDQ+mD9q6JERJEIT2FMuF/fBGfkhIR/g== + version "5.56.0" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.56.0.tgz#675640fcc780105cd22d3faa738b5d7ea6426f61" + integrity sha512-MfKVmYgifXjQpLSgpETuih7A7WTTIsxvKfSLGseTY5+qt0E1UD1wblZGM6WLenORo8sgmf+3X+WTe2WF7mufyw== collapse-white-space@^1.0.2: version "1.0.6" @@ -3729,9 +3656,9 @@ color@^3.0.0: color-string "^1.5.2" colorette@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.0.tgz#45306add826d196e8c87236ac05d797f25982e63" - integrity sha512-soRSroY+OF/8OdA3PTQXwaDJeMc7TfknKKrxeSCencL2a4+Tx5zhxmmv7hdpCjhKBjehzp8+bwe/T68K0hpIjw== + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== colors@^1.1.2: version "1.4.0" @@ -3948,15 +3875,13 @@ constant-case@^2.0.0: snake-case "^2.1.0" upper-case "^1.1.1" -constantinople@^3.0.1, constantinople@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.1.2.tgz#d45ed724f57d3d10500017a7d3a889c1381ae647" - integrity sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw== +constantinople@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151" + integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw== dependencies: - "@types/babel-types" "^7.0.0" - "@types/babylon" "^6.16.2" - babel-types "^6.26.0" - babylon "^6.18.0" + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.1" constants-browserify@^1.0.0: version "1.0.0" @@ -4040,11 +3965,6 @@ core-js-compat@^3.6.2, core-js-compat@^3.6.5: browserslist "^4.8.5" semver "7.0.0" -core-js-pure@^3.0.0: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" - integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== - core-js@2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.0.tgz#1e30793e9ee5782b307e37ffa22da0eacddd84d4" @@ -4398,9 +4318,9 @@ cssstyle@^2.0.0: cssom "~0.3.6" csstype@^2.6.5: - version "2.6.11" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.11.tgz#452f4d024149ecf260a852b025e36562a253ffc5" - integrity sha512-l8YyEC9NBkSm783PFTvh0FmJy7s5pFKrDp49ZL7zBGX3fWkO+N4EEyan1qqp8cwPLDcD0OSdyY6hAMoxp34JFw== + version "2.6.13" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f" + integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A== cucumber-html-reporter@^3.0.4: version "3.0.4" @@ -4519,18 +4439,11 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decamelize@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-3.2.0.tgz#84b8e8f4f8c579f938e35e2cc7024907e0090851" - integrity sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw== - dependencies: - xregexp "^4.2.4" - decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -4985,10 +4898,10 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.483: - version "1.3.487" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.487.tgz#8075e6ea33ee2e79a2dfb2a2467033f014017258" - integrity sha512-m4QS3IDShxauFfYFpnEzRCcUI55oKB9acEnHCuY/hSCZMz9Pz2KJj+UBnGHxRxS/mS1aphqOQ5wI6gc3yDZ7ew== +electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.488: + version "1.3.514" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.514.tgz#107499c28cb3c09fe6a863c19fc2202d5d9e8e41" + integrity sha512-8vb8zKIeGlZigeDzNWWthmGeLzo5CC43Lc+CZshMs7UXFVMPNLtXJGa/txedpu3OJFrXXVheBwp9PqOJJlHQ8w== elegant-spinner@^1.0.1: version "1.0.1" @@ -5039,11 +4952,11 @@ encodeurl@~1.0.2: integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= encoding@^0.1.11: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== dependencies: - iconv-lite "~0.4.13" + iconv-lite "^0.6.2" end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.4" @@ -5061,10 +4974,10 @@ enhanced-resolve@^0.9.1: memory-fs "^0.2.0" tapable "^0.1.8" -enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz#5d43bda4a0fd447cb0ebbe71bef8deff8805ad0d" - integrity sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ== +enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== dependencies: graceful-fs "^4.1.2" memory-fs "^0.5.0" @@ -5145,9 +5058,9 @@ es6-promisify@^5.0.0: es6-promise "^4.0.3" escalade@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.1.tgz#52568a77443f6927cd0ab9c73129137533c965ed" - integrity sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" + integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== escape-html@~1.0.3: version "1.0.3" @@ -5429,9 +5342,9 @@ eventemitter3@^4.0.0: integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== events@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" - integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" + integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== eventsource@^1.0.7: version "1.0.7" @@ -6650,9 +6563,9 @@ graphql-static-binding@0.9.3: cucumber-html-reporter "^3.0.4" graphql-tag@^2.10.3: - version "2.10.3" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03" - integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA== + version "2.11.0" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd" + integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA== graphql@0.11.3: version "0.11.3" @@ -6676,16 +6589,16 @@ graphql@^0.13.1: iterall "^1.2.1" graphql@^14.0.0, graphql@^14.0.2: - version "14.6.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.6.0.tgz#57822297111e874ea12f5cd4419616930cd83e49" - integrity sha512-VKzfvHEKybTKjQVpTFrA5yUq2S9ihcZvfJAtsDBBCuV6wauPu1xl/f9ehgVf0FcEJJs4vz6ysb/ZMkGigQZseg== + version "14.7.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72" + integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA== dependencies: iterall "^1.2.2" graphql@^15.0.0: - version "15.2.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.2.0.tgz#d9c655a523a3196d4b23657ec6ec5963b3bd4970" - integrity sha512-tsceRyHfgzZo+ee0YK3o8f0CR0cXAXxRlxoORWFo/CoM1bVy3UXGWeyzBcf+Y6oqPvO27BDmOEVATcunOO/MrQ== + version "15.3.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278" + integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w== growl@1.10.5: version "1.10.5" @@ -6719,11 +6632,11 @@ har-schema@^2.0.0: integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= har-validator@~5.1.0, har-validator@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== dependencies: - ajv "^6.5.5" + ajv "^6.12.3" har-schema "^2.0.0" has-ansi@^2.0.0: @@ -6847,14 +6760,14 @@ hex-color-regex@^1.1.0: integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== highlight.js@^9.6.0: - version "9.18.1" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c" - integrity sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg== + version "9.18.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634" + integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ== -highlight.js@~9.16.0: - version "9.16.2" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.16.2.tgz#68368d039ffe1c6211bcc07e483daf95de3e403e" - integrity sha512-feMUrVLZvjy0oC7FVJQcSQRqbBq9kwqnYE4+Kj9ZjbHh3g+BisiPgF49NyQbVLNdrL/qqZr3Ca9yOKwgn2i/tw== +highlight.js@~10.1.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.1.2.tgz#c20db951ba1c22c055010648dfffd7b2a968e00c" + integrity sha512-Q39v/Mn5mfBlMff9r+zzA+gWxRsCRKwEMvYTiisLr/XUiFI/4puWt0Ojdko3R3JCNWGdOWaA5g/Yxqa23kC5AA== hmac-drbg@^1.0.0: version "1.0.1" @@ -7064,17 +6977,24 @@ human-signals@^1.1.1: integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== hyphenate-style-name@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" - integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" + integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" + integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -7293,9 +7213,9 @@ inquirer@6.5.0: through "^2.3.6" inquirer@^7.0.0, inquirer@^7.1.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.0.tgz#aa3e7cb0c18a410c3c16cdd2bc9dcbe83c4d333e" - integrity sha512-K+LZp6L/6eE5swqIcVXrxl21aGDU4S50gKH0/d96OMQnSBCyGyZl/oZhbkVmdp5sBoINHd4xZvFSARh2dk6DWA== + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.0" @@ -7303,7 +7223,7 @@ inquirer@^7.0.0, inquirer@^7.1.0: cli-width "^3.0.0" external-editor "^3.0.3" figures "^3.0.0" - lodash "^4.17.15" + lodash "^4.17.19" mute-stream "0.0.8" run-async "^2.4.0" rxjs "^6.6.0" @@ -7530,13 +7450,13 @@ is-docker@^2.0.0: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== -is-expression@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-3.0.0.tgz#39acaa6be7fd1f3471dc42c7416e61c24317ac9f" - integrity sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8= +is-expression@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab" + integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A== dependencies: - acorn "~4.0.2" - object-assign "^4.0.1" + acorn "^7.1.1" + object-assign "^4.1.1" is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" @@ -7889,9 +7809,9 @@ javascript-stringify@^2.0.0, javascript-stringify@^2.0.1: integrity sha512-yV+gqbd5vaOYjqlbk16EG89xB5udgjqQF3C5FAORDg4f/IS1Yc5ERCv5e/57yBcfJYw05V5JyIXabhwb75Xxow== javascript-time-ago@^2.0.4: - version "2.0.8" - resolved "https://registry.yarnpkg.com/javascript-time-ago/-/javascript-time-ago-2.0.8.tgz#708e5d507b9e1a4ea0c25987b0d9a1a86e8ee229" - integrity sha512-/cQbnAwmF2OgpMCg8r185ZGqkkoHE8paNn9T3S98A6DXFQn4irzMVgMSWnKB4jvcvzTauF3HRi9wZXUdwHyj6Q== + version "2.0.13" + resolved "https://registry.yarnpkg.com/javascript-time-ago/-/javascript-time-ago-2.0.13.tgz#aa11f80887ce044f851a78c8761861e436ed0954" + integrity sha512-zH+obXUQ4vlc9UlERFe637rNJQaVYLizwODUfGzYN/cNW/owkk5wzb327gAfEXFpI4yhFcStEaoqoJtMGAmrAg== dependencies: relative-time-format "^0.1.3" @@ -7904,9 +7824,9 @@ jest-worker@^25.4.0: supports-color "^7.0.0" js-base64@^2.1.8, js-base64@^2.1.9, js-base64@^2.3.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.2.tgz#cf9301bc5cc756892a9a6c8d7138322e5944fb0d" - integrity sha512-1hgLrLIrmCgZG+ID3VoLNLOSwjGnoZa8tyrUdEteMeIzsT6PH7PMLyUvbDwzNE56P3PNxyvuIOx4Uh2E5rzQIw== + version "2.6.3" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.3.tgz#7afdb9b57aa7717e15d370b66e8f36a9cb835dc3" + integrity sha512-fiUvdfCaAXoQTHdKMgTvg6IkecXDcVz6V5rlftUTclF9IKBjMizvSdQaCl/z/6TApDeby5NL+axYou3i0mu1Pg== js-beautify@^1.6.12: version "1.11.0" @@ -7931,7 +7851,7 @@ js-queue@2.0.0: dependencies: easy-stack "^1.0.0" -js-stringify@^1.0.1: +js-stringify@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds= @@ -8304,11 +8224,6 @@ lazy-ass@1.6.0: resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= - lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" @@ -8618,7 +8533,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15, lodash@^4.0.0, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10: +lodash@4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -8628,6 +8543,11 @@ lodash@4.17.5: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw== +lodash@^4.0.0, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + log-symbols@2.2.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -8675,11 +8595,6 @@ longest-streak@^2.0.1: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= - loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -8717,13 +8632,13 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lowlight@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.13.1.tgz#c4f0e03906ebd23fedf2d258f6ab2f6324cf90eb" - integrity sha512-kQ71/T6RksEVz9AlPq07/2m+SU/1kGvt9k39UtvHX760u4SaWakaYH7hYgH5n6sTsCWk4MVYzUzLU59aN5CSmQ== +lowlight@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.14.0.tgz#83ebc143fec0f9e6c0d3deffe01be129ce56b108" + integrity sha512-N2E7zTM7r1CwbzwspPxJvmjAbxljCPThTFawEX2Z7+P3NGrrvY54u8kyU16IY4qWfoVIxY8SYCS8jTkuG7TqYA== dependencies: fault "^1.0.0" - highlight.js "~9.16.0" + highlight.js "~10.1.0" lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3, lru-cache@^4.1.5: version "4.1.5" @@ -9094,9 +9009,9 @@ minipass-flush@^1.0.5: minipass "^3.0.0" minipass-pipeline@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz#55f7839307d74859d6e8ada9c3ebe72cec216a34" - integrity sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== dependencies: minipass "^3.0.0" @@ -9158,7 +9073,7 @@ mkdirp@0.5.4: dependencies: minimist "^1.2.5" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -9240,9 +9155,9 @@ move-concurrently@^1.0.1: run-queue "^1.0.3" mri@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.5.tgz#ce21dba2c69f74a9b7cf8a1ec62307e089e223e0" - integrity sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg== + version "1.1.6" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6" + integrity sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ== ms@2.0.0: version "2.0.0" @@ -9324,9 +9239,9 @@ negotiator@0.6.2: integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== ngeohash@^0.6.3: version "0.6.3" @@ -9440,9 +9355,9 @@ node-ipc@^9.1.1: vm-browserify "^1.0.1" node-releases@^1.1.29, node-releases@^1.1.3, node-releases@^1.1.58: - version "1.1.58" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" - integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg== + version "1.1.60" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" + integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== node-request-by-swagger@^1.0.6: version "1.1.4" @@ -9842,9 +9757,9 @@ ora@^3.0.0, ora@^3.4.0: wcwidth "^1.0.1" ora@^4.0.2: - version "4.0.4" - resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d" - integrity sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww== + version "4.0.5" + resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.5.tgz#7410b5cc2d99fa637fd5099bbb9f02bfbb5a361e" + integrity sha512-jCDgm9DqvRcNIAEv2wZPrh7E5PcQiDUnbnWbAfu4NGAE2ZNqPFbDixmWldy1YG2QfLeQhuiu6/h5VRrk6cG50w== dependencies: chalk "^3.0.0" cli-cursor "^3.1.0" @@ -10100,9 +10015,9 @@ parse-json@^4.0.0: json-parse-better-errors "^1.0.1" parse-json@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" - integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== + version "5.0.1" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.1.tgz#7cfe35c1ccd641bce3981467e6c2ece61b3b3878" + integrity sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ== dependencies: "@babel/code-frame" "^7.0.0" error-ex "^1.3.1" @@ -10275,9 +10190,9 @@ performance-now@^2.1.0: integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= phoenix@^1.4.11: - version "1.5.3" - resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.5.3.tgz#994487b67adc8ca683f76c41dca3271e476184bd" - integrity sha512-wcyHTac54uxpyycKsdXfSpN+5YuYwaXQnkLahxseznDn+V5K3mIEJg+d6QhLBdrn5/0pWIPE7bBC6SBswnIvOg== + version "1.5.4" + resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.5.4.tgz#d42bb537f03f55076b4e7a6757fe29318a8439f0" + integrity sha512-mTxseCKWDgrBQRIriqzvxL+QH5xruu6KQPqFdDx0jrdu/nqWCo914MLihVksn7SV2Bol3T+e/VtovJgC5UZT+w== picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" @@ -10364,13 +10279,13 @@ popper.js@^1.15.0: integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== portfinder@^1.0.26: - version "1.0.26" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" - integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== + version "1.0.28" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== dependencies: async "^2.6.2" debug "^3.1.1" - mkdirp "^0.5.1" + mkdirp "^0.5.5" posix-character-classes@^0.1.0: version "0.1.1" @@ -10530,14 +10445,14 @@ postcss-modules-local-by-default@^2.0.6: postcss-value-parser "^3.3.1" postcss-modules-local-by-default@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915" - integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ== + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== dependencies: icss-utils "^4.1.1" - postcss "^7.0.16" + postcss "^7.0.32" postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.0" + postcss-value-parser "^4.1.0" postcss-modules-scope@^2.1.0, postcss-modules-scope@^2.2.0: version "2.2.0" @@ -10722,7 +10637,7 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== @@ -10737,7 +10652,7 @@ postcss@^5.2.17: source-map "^0.5.6" supports-color "^3.2.3" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -10762,9 +10677,9 @@ posthtml-rename-id@^1.0: escape-string-regexp "1.0.5" posthtml-render@^1.0.5, posthtml-render@^1.0.6: - version "1.2.2" - resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.2.2.tgz#f554a19ed40d40e2bfc160826b0a91d4a23656cd" - integrity sha512-MbIXTWwAfJ9qET6Zl29UNwJcDJEEz9Zkr5oDhiujitJa7YBJwEpbkX2cmuklCDxubTMoRWpid3q8DrSyGnUUzQ== + version "1.2.3" + resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.2.3.tgz#da1cf7ba4efb42cfe9c077f4f41669745de99b6d" + integrity sha512-rGGayND//VwTlsYKNqdILsA7U/XP0WJa6SMcdAEoqc2WRM5QExplGg/h9qbTuHz7mc2PvaXU+6iNxItvr5aHMg== posthtml-svg-mode@^1.0.3: version "1.0.3" @@ -10941,14 +10856,14 @@ prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" -prosemirror-collab@1.2.2: +prosemirror-collab@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.2.2.tgz#8d2c0e82779cfef5d051154bd0836428bd6d9c4a" integrity sha512-tBnHKMLgy5Qmx9MYVcLfs3pAyjtcqYYDd9kp3y+LSiQzkhMQDfZSV3NXWe4Gsly32adSef173BvObwfoSQL5MA== dependencies: prosemirror-state "^1.0.0" -prosemirror-commands@1.1.4: +prosemirror-commands@1.1.4, prosemirror-commands@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.1.4.tgz#991563e67623acab4f8c510fad1570f8b4693780" integrity sha512-kj4Qi+8h3EpJtZuuEDwZ9h2/QNGWDsIX/CzjmClxi9GhxWyBUMVUvIFk0mgdqHyX20lLeGmOpc0TLA5aPzgpWg== @@ -10976,7 +10891,7 @@ prosemirror-gapcursor@1.1.5: prosemirror-state "^1.0.0" prosemirror-view "^1.0.0" -prosemirror-history@1.1.3: +prosemirror-history@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.1.3.tgz#4f76a1e71db4ef7cdf0e13dec6d8da2aeaecd489" integrity sha512-zGDotijea+vnfnyyUGyiy1wfOQhf0B/b6zYcCouBV8yo6JmrE9X23M5q7Nf/nATywEZbgRLG70R4DmfSTC+gfg== @@ -10985,7 +10900,7 @@ prosemirror-history@1.1.3: prosemirror-transform "^1.0.0" rope-sequence "^1.3.0" -prosemirror-inputrules@1.1.2: +prosemirror-inputrules@1.1.2, prosemirror-inputrules@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.1.2.tgz#487e46c763e1212a4577397aba7706139084f012" integrity sha512-Ja5Z3BWestlHYGvtSGqyvxMeB8QEuBjlHM8YnKtLGUXMDp965qdDV4goV8lJb17kIWHk7e7JNj6Catuoa3302g== @@ -10993,15 +10908,7 @@ prosemirror-inputrules@1.1.2: prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-keymap@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.3.tgz#be22d6108df2521608e9216a87b1a810f0ed361e" - integrity sha512-PRA4NzkUMzV/NFf5pyQ6tmlIHiW/qjQ1kGWUlV2rF/dvlOxtpGpTEjIMhWgLuMf+HiDEFnUEP7uhYXu+t+491g== - dependencies: - prosemirror-state "^1.0.0" - w3c-keyname "^2.2.0" - -prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2: +prosemirror-keymap@1.1.4, prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.4.tgz#8b481bf8389a5ac40d38dbd67ec3da2c7eac6a6d" integrity sha512-Al8cVUOnDFL4gcI5IDlG6xbZ0aOD/i3B17VT+1JbHWDguCgt/lBHVTHUBcKvvbSg6+q/W4Nj1Fu6bwZSca3xjg== @@ -11009,14 +10916,14 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2: prosemirror-state "^1.0.0" w3c-keyname "^2.2.0" -prosemirror-model@1.9.1, prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.8.1: +prosemirror-model@1.10.0, prosemirror-model@1.9.1, prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.10.0, prosemirror-model@^1.8.1: version "1.9.1" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.9.1.tgz#8c08cf556f593c5f015548d2c1a6825661df087f" integrity sha512-Qblh8pm1c7Ll64sYLauwwzjimo/tFg1zW3Q3IWhKRhvfOEgRKqa6dC5pRrAa+XHOIjBFEYrqbi52J5bqA2dV8Q== dependencies: orderedmap "^1.1.0" -prosemirror-schema-list@1.1.2: +prosemirror-schema-list@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.1.2.tgz#310809209094b03425da7f5c337105074913da6c" integrity sha512-dgM9PwtM4twa5WsgSYMB+J8bwjnR43DAD3L9MsR9rKm/nZR5Y85xcjB7gusVMSsbQ2NomMZF03RE6No6mTnclQ== @@ -11024,7 +10931,7 @@ prosemirror-schema-list@1.1.2: prosemirror-model "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1: +prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.3.3.tgz#b2862866b14dec2b3ae1ab18229f2bd337651a2c" integrity sha512-PLXh2VJsIgvlgSTH6I2Yg6vk1CzPDp21DFreVpQtDMY2S6WaMmrQgDTLRcsrD8X38v8Yc873H7+ogdGzyIPn+w== @@ -11032,10 +10939,10 @@ prosemirror-state@1.3.3, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, pro prosemirror-model "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-tables@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.0.0.tgz#ec3d0b11e638c6a92dd14ae816d0a2efd1719b70" - integrity sha512-zFw5Us4G5Vdq0yIj8GiqZOGA6ud5UKpMKElux9O0HrfmhkuGa1jf1PCpz2R5pmIQJv+tIM24H1mox/ODBAX37Q== +prosemirror-tables@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.1.0.tgz#e7fc65e57a44759b0b999d8c71294f79e5a4d54b" + integrity sha512-E00+KSbDw65966GdiLBpqTNxIextw0RavlGmvdv/dyYbN9OTD0gzaoCU1S8MAbz4GLKmY9Y/g4nSiC1IL1ThQg== dependencies: prosemirror-keymap "^1.1.2" prosemirror-model "^1.8.1" @@ -11043,38 +10950,31 @@ prosemirror-tables@1.0.0: prosemirror-transform "^1.2.1" prosemirror-view "^1.13.3" -prosemirror-transform@1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.5.tgz#7a3e2c61fcdbaf1d0844a2a3bc34fc3524e9809c" - integrity sha512-eqeIaxWtUfOnpA1ERrXCuSIMzqIJtL9Qrs5uJMCjY5RMSaH5o4pc390SAjn/IDPeIlw6auh0hCCXs3wRvGnQug== +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.7.tgz#ba0e291a3cb43e6b633b779d93f53d01f5dad570" + integrity sha512-/107Lo2zeDgXuJBxb8s/clNu0Z2W8Gv3MKmkuSS/68Mcr7LBaUnN/Hj2g+GUxEJ7MpExCzFs65GrsNo2K9rxUQ== dependencies: prosemirror-model "^1.0.0" -prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1: - version "1.2.6" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.6.tgz#b3ad86e976c49f7dd541cc39e0d7215bcfe7b596" - integrity sha512-DyV6cRip8//GIHTrqBe2B7I8VjPFQZYuBuB4clpguq1SrS9lLponoBt/0XRWxETkCVsxYSvwE76X0zo6AZhwaw== - dependencies: - prosemirror-model "^1.0.0" - -prosemirror-utils@0.9.6: +prosemirror-utils@^0.9.6: version "0.9.6" resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973" integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA== -prosemirror-view@1.14.7: - version "1.14.7" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.14.7.tgz#5480f2ec7f091e616989894983b62c5e2d16edc1" - integrity sha512-ZCRbGAmJa0ORIY4xrDvOpxS/oAnph3egDauvQEI7SX4eex0zovUfC61I5X4AtPCaNN4JpLWEk60voCWi0cE2vA== +prosemirror-view@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.0.tgz#372102c91d05b3b0f371b3eb59aeacedb5011bba" + integrity sha512-a7Q76sO/DCZr2UX2Rv1Rbw52cr9kVIz8iJOf/rq4mPN1NA3lugq2BKJgUMwlB3U4utyw3olLigqouRHM48NJyg== dependencies: prosemirror-model "^1.1.0" prosemirror-state "^1.0.0" prosemirror-transform "^1.1.0" -prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3: - version "1.15.0" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.0.tgz#372102c91d05b3b0f371b3eb59aeacedb5011bba" - integrity sha512-a7Q76sO/DCZr2UX2Rv1Rbw52cr9kVIz8iJOf/rq4mPN1NA3lugq2BKJgUMwlB3U4utyw3olLigqouRHM48NJyg== +prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.15.2.tgz#3f07881d11f18c033467591bbaec26b569bbc22c" + integrity sha512-0wftmMDVD8VXj2HZgv6Rg//+tgJC0lpV9LkYlCiAkDLKsf4yW3Ozs5td1ZXqsyoqvX0ga/k5g2EyLbqOMmC1+w== dependencies: prosemirror-model "^1.1.0" prosemirror-state "^1.0.0" @@ -11125,110 +11025,108 @@ public-encrypt@^4.0.0: randombytes "^2.0.1" safe-buffer "^5.1.2" -pug-attrs@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-2.0.4.tgz#b2f44c439e4eb4ad5d4ef25cac20d18ad28cc336" - integrity sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ== +pug-attrs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41" + integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA== dependencies: - constantinople "^3.0.1" - js-stringify "^1.0.1" - pug-runtime "^2.0.5" + constantinople "^4.0.1" + js-stringify "^1.0.2" + pug-runtime "^3.0.0" -pug-code-gen@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.2.tgz#ad0967162aea077dcf787838d94ed14acb0217c2" - integrity sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw== +pug-code-gen@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.1.tgz#ff3b337b100c494ea63ef766091d27f7d73acb7e" + integrity sha512-xJIGvmXTQlkJllq6hqxxjRWcay2F9CU69TuAuiVZgHK0afOhG5txrQOcZyaPHBvSWCU/QQOqEp5XCH94rRZpBQ== dependencies: - constantinople "^3.1.2" + constantinople "^4.0.1" doctypes "^1.1.0" - js-stringify "^1.0.1" - pug-attrs "^2.0.4" - pug-error "^1.3.3" - pug-runtime "^2.0.5" - void-elements "^2.0.1" - with "^5.0.0" + js-stringify "^1.0.2" + pug-attrs "^3.0.0" + pug-error "^2.0.0" + pug-runtime "^3.0.0" + void-elements "^3.1.0" + with "^7.0.0" -pug-error@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-1.3.3.tgz#f342fb008752d58034c185de03602dd9ffe15fa6" - integrity sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ== +pug-error@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5" + integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ== -pug-filters@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-3.1.1.tgz#ab2cc82db9eeccf578bda89130e252a0db026aa7" - integrity sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg== +pug-filters@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e" + integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A== dependencies: - clean-css "^4.1.11" - constantinople "^3.0.1" + constantinople "^4.0.1" jstransformer "1.0.0" - pug-error "^1.3.3" - pug-walk "^1.1.8" - resolve "^1.1.6" - uglify-js "^2.6.1" + pug-error "^2.0.0" + pug-walk "^2.0.0" + resolve "^1.15.1" -pug-lexer@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-4.1.0.tgz#531cde48c7c0b1fcbbc2b85485c8665e31489cfd" - integrity sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA== +pug-lexer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.0.tgz#0b779e7d8cbf0f103803675be96351942fd9a727" + integrity sha512-52xMk8nNpuyQ/M2wjZBN5gXQLIylaGkAoTk5Y1pBhVqaopaoj8Z0iVzpbFZAqitL4RHNVDZRnJDsqEYe99Ti0A== dependencies: - character-parser "^2.1.1" - is-expression "^3.0.0" - pug-error "^1.3.3" + character-parser "^2.2.0" + is-expression "^4.0.0" + pug-error "^2.0.0" -pug-linker@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-3.0.6.tgz#f5bf218b0efd65ce6670f7afc51658d0f82989fb" - integrity sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg== +pug-linker@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708" + integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw== dependencies: - pug-error "^1.3.3" - pug-walk "^1.1.8" + pug-error "^2.0.0" + pug-walk "^2.0.0" -pug-load@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-2.0.12.tgz#d38c85eb85f6e2f704dea14dcca94144d35d3e7b" - integrity sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg== +pug-load@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662" + integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ== dependencies: - object-assign "^4.1.0" - pug-walk "^1.1.8" + object-assign "^4.1.1" + pug-walk "^2.0.0" -pug-parser@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-5.0.1.tgz#03e7ada48b6840bd3822f867d7d90f842d0ffdc9" - integrity sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA== +pug-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260" + integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw== dependencies: - pug-error "^1.3.3" - token-stream "0.0.1" + pug-error "^2.0.0" + token-stream "1.0.0" -pug-runtime@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-2.0.5.tgz#6da7976c36bf22f68e733c359240d8ae7a32953a" - integrity sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw== +pug-runtime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.0.tgz#d523025fdc0a1efe70929d1fd3a2d24121ffffb6" + integrity sha512-GoEPcmQNnaTsePEdVA05bDpY+Op5VLHKayg08AQiqJBWU/yIaywEYv7TetC5dEQS3fzBBoyb2InDcZEg3mPTIA== -pug-strip-comments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz#cc1b6de1f6e8f5931cf02ec66cdffd3f50eaf8a8" - integrity sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw== +pug-strip-comments@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e" + integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ== dependencies: - pug-error "^1.3.3" + pug-error "^2.0.0" -pug-walk@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-1.1.8.tgz#b408f67f27912f8c21da2f45b7230c4bd2a5ea7a" - integrity sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA== +pug-walk@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe" + integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ== -pug@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pug/-/pug-2.0.4.tgz#ee7682ec0a60494b38d48a88f05f3b0ac931377d" - integrity sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw== +pug@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.0.tgz#101eecd7a236cd9906e420e17799d4d57f2b7d93" + integrity sha512-inmsJyFBSHZaiGLaguoFgJGViX0If6AcfcElimvwj9perqjDpUpw79UIEDZbWFmoGVidh08aoE+e8tVkjVJPCw== dependencies: - pug-code-gen "^2.0.2" - pug-filters "^3.1.1" - pug-lexer "^4.1.0" - pug-linker "^3.0.6" - pug-load "^2.0.12" - pug-parser "^5.0.1" - pug-runtime "^2.0.5" - pug-strip-comments "^1.0.4" + pug-code-gen "^3.0.0" + pug-filters "^4.0.0" + pug-lexer "^5.0.0" + pug-linker "^4.0.0" + pug-load "^3.0.0" + pug-parser "^6.0.0" + pug-runtime "^3.0.0" + pug-strip-comments "^2.0.0" pump@^2.0.0: version "2.0.1" @@ -11678,12 +11576,12 @@ readdirp@~3.4.0: dependencies: picomatch "^2.2.1" -recast@^0.17.3: - version "0.17.6" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.17.6.tgz#64ae98d0d2dfb10ff92ff5fb9ffb7371823b69fa" - integrity sha512-yoQRMRrK1lszNtbkGyM4kN45AwylV5hMiuEveUBlxytUViWevjvX6w+tzJt1LH4cfUhWt4NZvy3ThIhu6+m5wQ== +recast@0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.19.1.tgz#555f3612a5a10c9f44b9a923875c51ff775de6c8" + integrity sha512-8FCjrBxjeEU2O6I+2hyHyBFH1siJbMBLwIRvVr1T3FD2cL754sOaJDsJ/8h3xYltasbJ8jqWRIhMuDGBSiSbjw== dependencies: - ast-types "0.12.4" + ast-types "0.13.3" esprima "~4.0.0" private "^0.1.8" source-map "~0.6.1" @@ -11741,9 +11639,9 @@ regenerator-runtime@^0.12.0: integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== regenerator-runtime@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== regenerator-transform@^0.14.2: version "0.14.5" @@ -11804,9 +11702,9 @@ registry-auth-token@^3.0.1: safe-buffer "^5.0.1" registry-auth-token@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" - integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA== + version "4.2.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" + integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w== dependencies: rc "^1.2.8" @@ -11917,7 +11815,7 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== -repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: +repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -11946,29 +11844,29 @@ request-progress@3.0.0: dependencies: throttleit "^1.0.0" -request-promise-core@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" - integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== +request-promise-core@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" + integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== dependencies: - lodash "^4.17.15" + lodash "^4.17.19" request-promise-native@^1.0.7: - version "1.0.8" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" - integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== + version "1.0.9" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" + integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== dependencies: - request-promise-core "1.1.3" + request-promise-core "1.1.4" stealthy-require "^1.1.1" tough-cookie "^2.3.3" request-promise@^4.1.1: - version "4.2.5" - resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.5.tgz#186222c59ae512f3497dfe4d75a9c8461bd0053c" - integrity sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg== + version "4.2.6" + resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.6.tgz#7e7e5b9578630e6f598e3813c0f8eb342a27f0a2" + integrity sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ== dependencies: bluebird "^3.5.0" - request-promise-core "1.1.3" + request-promise-core "1.1.4" stealthy-require "^1.1.1" tough-cookie "^2.3.3" @@ -12079,7 +11977,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.8.1: +resolve@^1.10.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.8.1: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -12147,13 +12045,6 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= - dependencies: - align-text "^0.1.1" - rimraf@2, rimraf@2.7.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -12243,7 +12134,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -12396,7 +12287,7 @@ sentence-case@^2.1.0: no-case "^2.2.0" upper-case-first "^1.1.2" -serialize-javascript@^2.1.0, serialize-javascript@^2.1.2: +serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== @@ -12408,6 +12299,13 @@ serialize-javascript@^3.1.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + serializerr@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91" @@ -12693,7 +12591,7 @@ source-map@^0.4.2: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.1: +source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -13071,9 +12969,9 @@ strip-json-comments@2.0.1, strip-json-comments@~2.0.1: integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= strip-json-comments@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" - integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== style-loader@^1.0.0: version "1.2.1" @@ -13405,68 +13303,68 @@ tiny-warning@^1.0.2: integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== tippy.js@^6.2.3: - version "6.2.4" - resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.4.tgz#b76000080cc035745cab4f20ac7a30e1ccb1b6bb" - integrity sha512-S3qLJhx7cpeGDpHw411jU62W1RvOGPkt3r68y8nwPi7wm/aexrSYAADbVb1ZNYCjspEwLWQvtAx76COvPcLvCw== + version "6.2.6" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.6.tgz#4991bbe8f75e741fb92b5ccfeebcd072d71f8345" + integrity sha512-0tTL3WQNT0nWmpslhDryRahoBm6PT9fh1xXyDfOsvZpDzq52by2rF2nvsW0WX2j9nUZP/jSGDqfKJGjCtoGFKg== dependencies: - "@popperjs/core" "^2.3.2" + "@popperjs/core" "^2.4.4" -tiptap-commands@^1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.13.1.tgz#b605292aa333192f33c7c50079c96262cdc7ff2a" - integrity sha512-bniLSrnxId9zlcuwo4lVA4cmUgHpDzDhz5yw6ubDP0O++xtHl96me5E3lje8VAgBbMvP1txwwu6xSUp8xrAuXQ== +tiptap-commands@^1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.14.3.tgz#84d6fbecaa642cd2ef44d6024569c211866b8567" + integrity sha512-4Nvs3vDf1UM5bvhko/7yQH3gN0QePWqJn4Q34N5t1sabwfF7bObyn/lIvQZvbUP3M0N1pUcWqzm6bOXlQ38yCQ== dependencies: - prosemirror-commands "1.1.4" - prosemirror-inputrules "1.1.2" - prosemirror-model "1.9.1" - prosemirror-schema-list "1.1.2" - prosemirror-state "1.3.3" - prosemirror-tables "1.0.0" - prosemirror-utils "0.9.6" - tiptap-utils "^1.9.1" + prosemirror-commands "^1.1.4" + prosemirror-inputrules "^1.1.2" + prosemirror-model "^1.10.0" + prosemirror-schema-list "^1.1.2" + prosemirror-state "^1.3.3" + prosemirror-tables "^1.1.0" + prosemirror-utils "^0.9.6" + tiptap-utils "^1.10.3" tiptap-extensions@^1.29.1: - version "1.29.1" - resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.29.1.tgz#70679bd57ffcc6a67358f58d03e574b40e0432bb" - integrity sha512-xwBvlGAN0W9+F5DB/s/pH8LcOaUq7WgPffv7KOGU26jmPKq8JAXXwZXn8DOmPYaRo9RscF0Tg9AADOM+0vcLkQ== + version "1.31.3" + resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.31.3.tgz#bebaaee69e87f6f85f5d0ab17f84bc48a1f6c767" + integrity sha512-brS9ptnFxqJuE9MpwUP0LucptvhPcIhDkWNMftnWplbHOml5xEL6zR40FYkmmCq+ZJRh6+kS5HI9HYRp3StXMw== dependencies: - lowlight "1.13.1" - prosemirror-collab "1.2.2" - prosemirror-history "1.1.3" - prosemirror-model "1.9.1" - prosemirror-state "1.3.3" - prosemirror-tables "1.0.0" - prosemirror-transform "1.2.5" - prosemirror-utils "0.9.6" - prosemirror-view "1.14.7" - tiptap "^1.27.1" - tiptap-commands "^1.13.1" + lowlight "^1.14.0" + prosemirror-collab "^1.2.2" + prosemirror-history "^1.1.3" + prosemirror-model "^1.10.0" + prosemirror-state "^1.3.3" + prosemirror-tables "^1.1.0" + prosemirror-transform "^1.2.6" + prosemirror-utils "^0.9.6" + prosemirror-view "^1.15.0" + tiptap "^1.29.3" + tiptap-commands "^1.14.3" -tiptap-utils@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.9.1.tgz#1adf749ecf417b3db4972afb7fa55dcef1329382" - integrity sha512-E0tRFTNRYYwFRBhmSEjOUFmMnEyUD5rZ2QjiJaxf4ZXAPiUVy3gt2J7DqBjeP1q1FsmXkkkAHsxV+5hqX/lfFg== +tiptap-utils@^1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.10.3.tgz#3b902530452a3522d5c3f2f49833b13103f9703b" + integrity sha512-UmEOTPgUeS9qali1wOXY5gqhxNjZ5vDe7vGUZ99oPN1LsavEGx4kg+O0OqerlLkQv0gcRG88BMZouhhNzg87Zw== dependencies: - prosemirror-model "1.9.1" - prosemirror-state "1.3.3" - prosemirror-tables "1.0.0" - prosemirror-utils "0.9.6" + prosemirror-model "^1.10.0" + prosemirror-state "^1.3.3" + prosemirror-tables "^1.1.0" + prosemirror-utils "^0.9.6" -tiptap@^1.26.0, tiptap@^1.27.1: - version "1.27.1" - resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.27.1.tgz#b40b6634f23913b4570d6e7a8ed4eb5b206c7589" - integrity sha512-CwPMwKAKjAzsnkxZSISqDh73JmTZP3qpYn91k71WfIUZ7KUDkDt8gOKDrHMhaTJR2qMmuAChkkzd3OvBaBX+/Q== +tiptap@^1.26.0, tiptap@^1.29.3: + version "1.29.3" + resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.29.3.tgz#690dee9bcfc38d1f8d1c484de8d4d0314ceae996" + integrity sha512-1dngx+UsySlnfGHu8vDQpcrXcROzMMsFPGJrxSsf0/79dUgIdIIW01ZM8j0cIPKm+7BdXguhMRCxyN8u3NvgWA== dependencies: prosemirror-commands "1.1.4" prosemirror-dropcursor "1.3.2" prosemirror-gapcursor "1.1.5" prosemirror-inputrules "1.1.2" - prosemirror-keymap "1.1.3" - prosemirror-model "1.9.1" + prosemirror-keymap "1.1.4" + prosemirror-model "1.10.0" prosemirror-state "1.3.3" - prosemirror-view "1.14.7" - tiptap-commands "^1.13.1" - tiptap-utils "^1.9.1" + prosemirror-view "1.15.0" + tiptap-commands "^1.14.3" + tiptap-utils "^1.10.3" title-case@^2.1.0: version "2.1.1" @@ -13518,11 +13416,6 @@ to-ast@^1.0.0: ast-types "^0.7.2" esprima "^2.1.0" -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -13570,10 +13463,10 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -token-stream@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a" - integrity sha1-zu78cXp2xDFvEm0LnbqlXX598Bo= +token-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4" + integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ= toposort@^1.0.0: version "1.0.7" @@ -13803,9 +13696,9 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= typescript@^3.9.3, typescript@~3.9.3: - version "3.9.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" - integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== uglify-js@3.4.x: version "3.4.10" @@ -13815,21 +13708,6 @@ uglify-js@3.4.x: commander "~2.19.0" source-map "~0.6.1" -uglify-js@^2.6.1: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= - unherit@^1.0.4: version "1.1.3" resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" @@ -13950,9 +13828,9 @@ unist-util-visit-parents@^2.0.0: unist-util-is "^3.0.0" unist-util-visit-parents@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.0.2.tgz#d4076af3011739c71d2ce99d05de37d545f4351d" - integrity sha512-yJEfuZtzFpQmg1OSCyS9M5NJRrln/9FbYosH3iW0MG402QbdbaB8ZESwUv9RO6nRfLAKvWcMxCwdLWOov36x/g== + version "3.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz#4dd262fb9dcfe44f297d53e882fc6ff3421173d5" + integrity sha512-0g4wbluTF93npyPrp/ymd3tCDTMnP0yo2akFD2FIBAYXq/Sga3lwaU1D8OYKbtpioaI6CkDcQ6fsMnmtzt7htw== dependencies: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" @@ -13965,9 +13843,9 @@ unist-util-visit@^1.1.0: unist-util-visit-parents "^2.0.0" unist-util-visit@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.2.tgz#3843782a517de3d2357b4c193b24af2d9366afb7" - integrity sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ== + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" + integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== dependencies: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" @@ -14244,9 +14122,9 @@ vfile-message@^2.0.0: unist-util-stringify-position "^2.0.0" vfile@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.1.1.tgz#282d28cebb609183ac51703001bc18b3e3f17de9" - integrity sha512-lRjkpyDGjVlBA7cDQhQ+gNcvB1BGaTHYuSOcY3S7OhDmBtnzX95FhtZZDecSTDm6aajFymyve6S5DN4ZHGezdQ== + version "4.2.0" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.0.tgz#26c78ac92eb70816b01d4565e003b7e65a2a0e01" + integrity sha512-a/alcwCvtuc8OX92rqqo7PflxiCgXRFjdyoGVuYV+qbgCb0GgZJRvIgCD4+U/Kl1yhaRsaTwksF88xbPyGsgpw== dependencies: "@types/unist" "^2.0.0" is-buffer "^2.0.0" @@ -14259,31 +14137,31 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -void-elements@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" - integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= +void-elements@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= vue-apollo@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.3.tgz#7f29558df76eec0f03251847eef153816a261827" - integrity sha512-WJaQ1v/i46/oIPlKv7J0Tx6tTlbuaeCdhrAbL06h+Zca2gzr5ywjUFpl8ijMTGJsQ+Ph/U4xEpBFBOMxQmL+7g== + version "3.0.4" + resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.4.tgz#d48a990d02c1febb5248d097f921c74c50a22e8a" + integrity sha512-sthSS9E6FB7OMmSJmIG7e89QZvzwK/1PCD8A/IfGBST48pxY7sdSxRp22Gu2/s/gxQBnQPI1H1ZPZE97IG+zXA== dependencies: chalk "^2.4.2" - serialize-javascript "^2.1.0" + serialize-javascript "^4.0.0" throttle-debounce "^2.1.0" vue-class-component@^7.2.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.3.tgz#a5b1abd53513a72ad51098752e2dedd499807cca" - integrity sha512-oEqYpXKaFN+TaXU+mRLEx8dX0ah85aAJEe61mpdoUrq0Bhe/6sWhyZX1JjMQLhVsHAkncyhedhmCdDVSasUtDw== + version "7.2.5" + resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.5.tgz#212b3548c4fdd3314774c4adbc1c3792a40b52d0" + integrity sha512-0CSftHY0bDTD+4FbYkuFf6+iKDjZ4h2in2YYJDRMk5daZIjrgT9LjFHvP7Rzqy9/s1pij3zDtTSLRUjsPWMwqg== -vue-cli-plugin-styleguidist@~4.26.0: - version "4.26.0" - resolved "https://registry.yarnpkg.com/vue-cli-plugin-styleguidist/-/vue-cli-plugin-styleguidist-4.26.0.tgz#fd267f763e85939756b42db0e1a902343113db0b" - integrity sha512-cyS6cGwEG+wghZO8T06O6A2sUMMPpoVIbqQLMWqZTc+zU+A56+Ljo6ecPN0L9G0yaOv9MKXuIPZrAahLNGyxBQ== +vue-cli-plugin-styleguidist@~4.29.1: + version "4.29.1" + resolved "https://registry.yarnpkg.com/vue-cli-plugin-styleguidist/-/vue-cli-plugin-styleguidist-4.29.1.tgz#da4c1657e60dde359501cc880d638fd3efb8d9ad" + integrity sha512-POP+8kNKweWPZKd9/PuNt082WRLFO9gLKL9dGq2C0+yOr7jh9jejSknU5rDgk6vdyzIkTkgZfbg19FotmAue4Q== dependencies: - vue-styleguidist "^4.26.0" + vue-styleguidist "^4.29.1" webpack-merge "^4.2.1" vue-cli-plugin-svg@~0.1.3: @@ -14297,18 +14175,18 @@ vue-cli-plugin-svg@~0.1.3: url-loader "^2.0.0" vue-svg-loader "^0.12.0" -vue-docgen-api@^4.26.0: - version "4.26.0" - resolved "https://registry.yarnpkg.com/vue-docgen-api/-/vue-docgen-api-4.26.0.tgz#bf1a7fd201ddbcd62e4432a0e8b6369651fcf1fe" - integrity sha512-uJbmLup5NHukMUecMJiKgjLPdPIFDjlCFwOGX107S5gdlb+c/rd4ihOvWRAuVEBiUXWFxHJ12DZxCX1/UJGPcQ== +vue-docgen-api@^4.29.1: + version "4.29.1" + resolved "https://registry.yarnpkg.com/vue-docgen-api/-/vue-docgen-api-4.29.1.tgz#ee744d5636c3050d78b4f8ef5f49ca91c99bf869" + integrity sha512-DSN7d94shRr2p7YdfooMi+Wa8jP4QfA96REV+5zMLr3/cEu8rf6gXNS8FuR2+iv6hAeUsQfH4cmq/15oje+BDw== dependencies: "@babel/parser" "^7.6.0" "@babel/types" "^7.6.0" - ast-types "^0.12.2" + ast-types "0.13.3" hash-sum "^1.0.2" lru-cache "^4.1.5" - pug "^2.0.3" - recast "^0.17.3" + pug "^3.0.0" + recast "0.19.1" ts-map "^1.0.3" vue-template-compiler "^2.0.0" @@ -14342,27 +14220,27 @@ vue-i18n-extract@^1.0.2: js-yaml "^3.13.1" vue-i18n@^8.14.0: - version "8.18.2" - resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.18.2.tgz#cd7c12f2e178e6faa23b0e3cfd2f7bac9305f8fc" - integrity sha512-0X5nBTCZAVjlwcrPaYJwNs3iipBBTv0AUHwQUOa8yP3XbQGWKbRHqBb3OhCYtum/IHDD21d/df5Xd2VgyxbxfA== + version "8.20.0" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.20.0.tgz#c81b01d6541182b28565316cafe881b65a3c0f1b" + integrity sha512-ZiAOoeR4d/JtKpbjipx3I80ey7cYG1ki5gQ7HwzWm4YFio9brA15BEYHjalEoBaEfzF5OBEZP+Y2MvAaWnyXXg== -vue-inbrowser-compiler-utils@^4.23.3: - version "4.23.3" - resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.23.3.tgz#188f1a439d0b7776f4a4255076105239e0f31ee2" - integrity sha512-//NgjTyi9E2zEbVaXnkOHqKEpuCQ67HrEfvfLjBeuy9VOy07jH26FbdBG/bL3zA/iwXiHdiePSa1+YdvSyCKpA== +vue-inbrowser-compiler-utils@^4.27.0: + version "4.27.0" + resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.27.0.tgz#06f07b8fe9d2422a9ea652a508a7a77250430b56" + integrity sha512-NnsvgVQ0piyHO89RNjqyE/VrvFApItR5wHwYXjXwYVKSG6sx6ZyzAYhdW7JYC0rFoBz+kuTxdddDBDynnMEsCA== dependencies: camelcase "^5.3.1" -vue-inbrowser-compiler@^4.23.3: - version "4.23.3" - resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler/-/vue-inbrowser-compiler-4.23.3.tgz#2992366dc14cb7efca74fce8a410307eb662b544" - integrity sha512-ac5wMcvH0dpSeE2TsO7dY9zkyJ9UBB6R7U0IMsyWf6j1xSeHavCFtuOOQwphobLYlPqvneK1DoMwhVkDVm/fkQ== +vue-inbrowser-compiler@^4.27.0: + version "4.27.0" + resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler/-/vue-inbrowser-compiler-4.27.0.tgz#468be2c948b1df8a451db49d02f7e4d92b918c1d" + integrity sha512-P57562nGgzlF5YY53XKCtgDtjGwFVEzUyOomilzMIluhg9LGHeMgSlQZcWcSI39xyTrEPKwJQLTvTUOHXLjVZg== dependencies: acorn "^6.1.1" acorn-jsx "^5.0.1" buble "^0.19.7" camelcase "^5.3.1" - vue-inbrowser-compiler-utils "^4.23.3" + vue-inbrowser-compiler-utils "^4.27.0" walkes "^0.2.1" vue-loader@^15.9.2: @@ -14399,9 +14277,9 @@ vue-router@^3.1.6: integrity sha512-SdKRBeoXUjaZ9R/8AyxsdTqkOfMcI5tWxPZOUX5Ie1BTL5rPSZ0O++pbiZCeYeythiZIdLEfkDiQPKIaWk5hDg== vue-scrollto@^2.17.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/vue-scrollto/-/vue-scrollto-2.18.1.tgz#e23620489583f0b3f21410f149b6585a730672f2" - integrity sha512-JojeVOP1Z50Jt0OCOd61jRRRF8TrNvPAzF4LAKwkTEZnt30mpDd48aDZZOEPD3l6z2PybOyUGFT/FNWfTtFv9w== + version "2.18.2" + resolved "https://registry.yarnpkg.com/vue-scrollto/-/vue-scrollto-2.18.2.tgz#113a3548341974d3a2560145f39fcc0378533821" + integrity sha512-LQOsuCfDJ5JXsRBb6frM85sCFABZdkwwwchLh4k3wbp8P6URRDcQvwp31U4cTnbyYEOo6JFP7+3p0wkvwy7MdA== dependencies: bezier-easing "2.1.0" @@ -14413,13 +14291,13 @@ vue-style-loader@^4.1.0, vue-style-loader@^4.1.2: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue-styleguidist@^4.26.0: - version "4.26.0" - resolved "https://registry.yarnpkg.com/vue-styleguidist/-/vue-styleguidist-4.26.0.tgz#2e0be5a5bbbb276e0cfad1daffe0594ff7c25b23" - integrity sha512-/H/d47DMcCvSew88q3nXD+P5jlYvlBFqu162S9EqY4h3G1E1qaBXHxcKGAutJaEJNlWOWeNU8RrUmAau0Jb6Sw== +vue-styleguidist@^4.29.1: + version "4.29.1" + resolved "https://registry.yarnpkg.com/vue-styleguidist/-/vue-styleguidist-4.29.1.tgz#8fd8a3000e0be83525e0441eb549c9dd7133037a" + integrity sha512-CqWk8+qcetLX5l4NYQImmSQkkWkOoEGj715JJWfgkr8VQDU1aY28KWzxnlj+Dn1+RUttP/a+gQXqxqtoJPhHbQ== dependencies: "@vxna/mini-html-webpack-template" "^1.0.0" - ast-types "^0.13.2" + ast-types "^0.13.3" classnames "^2.2.6" clean-webpack-plugin "^3.0.0" cli-progress "^3.0.0" @@ -14466,9 +14344,9 @@ vue-styleguidist@^4.26.0: style-loader "^1.0.0" terser-webpack-plugin "^2.2.2" to-ast "^1.0.0" - vue-docgen-api "^4.26.0" - vue-inbrowser-compiler "^4.23.3" - vue-inbrowser-compiler-utils "^4.23.3" + vue-docgen-api "^4.29.1" + vue-inbrowser-compiler "^4.27.0" + vue-inbrowser-compiler-utils "^4.27.0" webpack-dev-server "^3.11.0" webpack-merge "^4.0.0" @@ -14503,7 +14381,7 @@ vue@>=2.0.0, vue@^2.6.11: resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== -vuedraggable@^2.23.2: +vuedraggable@2.23.2: version "2.23.2" resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.23.2.tgz#0d95d7fdf4f02f56755a26b3c9dca5c7ca9cfa72" integrity sha512-PgHCjUpxEAEZJq36ys49HfQmXglattf/7ofOzUrW2/rRdG7tu6fK84ir14t1jYv4kdXewTEa2ieKEAhhEMdwkQ== @@ -14543,15 +14421,15 @@ watchpack-chokidar2@^2.0.0: dependencies: chokidar "^2.1.8" -watchpack@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa" - integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g== +watchpack@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" + integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== dependencies: graceful-fs "^4.1.2" neo-async "^2.5.0" optionalDependencies: - chokidar "^3.4.0" + chokidar "^3.4.1" watchpack-chokidar2 "^2.0.0" wbuf@^1.1.0, wbuf@^1.7.3: @@ -14593,9 +14471,9 @@ webpack-bundle-analyzer@^3.8.0: ws "^6.0.0" webpack-chain@^6.4.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-6.5.0.tgz#0b4af2094a5058a9ccd34b8f7ab194de4c83365f" - integrity sha512-K4EHiEg4WlP4w1rKXKpYWvX9cfGBERHCGP06ETSNV62XUIfOUg1DDRQpxyBsFYxZLKc4YUAI3iiCIvWoliheGA== + version "6.5.1" + resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-6.5.1.tgz#4f27284cbbb637e3c8fbdef43eef588d4d861206" + integrity sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA== dependencies: deepmerge "^1.5.2" javascript-stringify "^2.0.1" @@ -14691,9 +14569,9 @@ webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack- source-map "~0.6.1" webpack@^4.0.0: - version "4.43.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" - integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== + version "4.44.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" + integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" @@ -14703,7 +14581,7 @@ webpack@^4.0.0: ajv "^6.10.2" ajv-keywords "^3.4.1" chrome-trace-event "^1.0.2" - enhanced-resolve "^4.1.0" + enhanced-resolve "^4.3.0" eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" loader-runner "^2.4.0" @@ -14716,7 +14594,7 @@ webpack@^4.0.0: schema-utils "^1.0.0" tapable "^1.1.3" terser-webpack-plugin "^1.4.3" - watchpack "^1.6.1" + watchpack "^1.7.4" webpack-sources "^1.4.1" websocket-driver@0.6.5: @@ -14753,9 +14631,9 @@ whatwg-fetch@2.0.4: integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== whatwg-fetch@>=0.10.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.1.0.tgz#49d630cdfa308dba7f2819d49d09364f540dbcc6" - integrity sha512-pgmbsVWKpH9GxLXZmtdowDIqtb/rvPyjjQv3z9wLcmgWKFHilKnZD3ldgrOlwJoPGOUluQsRPWd52yVkPfmI1A== + version "3.2.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.2.0.tgz#8e134f701f0a4ab5fda82626f113e2b647fd16dc" + integrity sha512-SdGPoQMMnzVYThUbSrEvqTlkvC1Ux27NehaJ/GUHBfNrh5Mjg+1/uRyFMwVnxO2MrikMWvWAqUGgQOfVU4hT7w== whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" @@ -14804,29 +14682,21 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= - -with@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/with/-/with-5.1.1.tgz#fa4daa92daf32c4ea94ed453c81f04686b575dfe" - integrity sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4= +with@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac" + integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w== dependencies: - acorn "^3.1.0" - acorn-globals "^3.0.0" + "@babel/parser" "^7.9.6" + "@babel/types" "^7.9.6" + assert-never "^1.2.1" + babel-walk "3.0.0-canary-5" word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= - workbox-background-sync@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-4.3.1.tgz#26821b9bf16e9e37fd1d640289edddc08afd1950" @@ -15031,9 +14901,9 @@ ws@^6.0.0, ws@^6.2.1: async-limiter "~1.0.0" ws@^7.0.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" - integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== + version "7.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" + integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== xdg-basedir@^3.0.0: version "3.0.0" @@ -15050,13 +14920,6 @@ xmlchars@^2.1.1: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xregexp@^4.2.4: - version "4.3.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" - integrity sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g== - dependencies: - "@babel/runtime-corejs3" "^7.8.3" - xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -15209,12 +15072,12 @@ yargs@^10.0.3: yargs-parser "^8.1.0" yargs@^15.0.0: - version "15.4.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.0.tgz#53949fb768309bac1843de9b17b80051e9805ec2" - integrity sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw== + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: cliui "^6.0.0" - decamelize "^3.2.0" + decamelize "^1.2.0" find-up "^4.1.0" get-caller-file "^2.0.1" require-directory "^2.1.1" @@ -15244,16 +15107,6 @@ yargs@^8.0.2: y18n "^3.2.1" yargs-parser "^7.0.0" -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" - yauzl@2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex index a71c9e262..a3b8c2014 100644 --- a/lib/federation/activity_pub/activity_pub.ex +++ b/lib/federation/activity_pub/activity_pub.ex @@ -13,41 +13,38 @@ defmodule Mobilizon.Federation.ActivityPub do alias Mobilizon.{ Actors, Config, - Conversations, + Discussions, Events, - Reports, Resources, Share, - Todos, Users } alias Mobilizon.Actors.{Actor, Follower, Member} - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.{Event, Participant} - alias Mobilizon.Reports.Report - alias Mobilizon.Resources.Resource - alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Tombstone alias Mobilizon.Federation.ActivityPub.{ Activity, Audience, Federator, + Fetcher, + Preloader, Relay, Transmogrifier, + Types, Visibility } + alias Mobilizon.Federation.ActivityPub.Types.{Managable, Ownable} + alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} - alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.HTTPSignatures.Signature alias Mobilizon.Federation.WebFinger - alias Mobilizon.GraphQL.API.Utils, as: APIUtils - alias Mobilizon.Service.Formatter.HTML alias Mobilizon.Service.Notifications.Scheduler - alias Mobilizon.Service.RichMedia.Parser + alias Mobilizon.Storage.Page alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Email.{Admin, Mailer} @@ -74,75 +71,44 @@ defmodule Mobilizon.Federation.ActivityPub do Fetch an object from an URL, from our local database of events and comments, then eventually remote """ # TODO: Make database calls parallel - @spec fetch_object_from_url(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()} - def fetch_object_from_url(url) do + @spec fetch_object_from_url(String.t(), Keyword.t()) :: + {:ok, struct()} | {:error, any()} + def fetch_object_from_url(url, options \\ []) do Logger.info("Fetching object from url #{url}") + force_fetch = Keyword.get(options, :force, false) 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, Conversations.get_comment_from_url(url)}, - {:existing_resource, nil} <- {:existing_resource, Resources.get_resource_by_url(url)}, - {:existing_actor, {:error, :actor_not_found}} <- - {:existing_actor, Actors.get_actor_by_url(url)}, - date <- Signature.generate_date_header(), - headers <- - [{:Accept, "application/activity+json"}] - |> maybe_date_fetch(date) - |> sign_fetch_relay(url, date), - {:ok, %HTTPoison.Response{body: body, status_code: code}} when code in 200..299 <- - HTTPoison.get( - url, - headers, - follow_redirect: true, - timeout: 10_000, - recv_timeout: 20_000, - ssl: [{:versions, [:"tlsv1.2"]}] - ), - {:ok, data} <- Jason.decode(body), - {:origin_check, true} <- {:origin_check, origin_check?(url, data)}, - params <- %{ - "type" => "Create", - "to" => data["to"], - "cc" => data["cc"], - "actor" => data["attributedTo"], - "object" => data - }, - {:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do - case data["type"] do - "Event" -> - {:ok, Events.get_public_event_by_url_with_preload!(object_url)} - - "Note" -> - {:ok, Conversations.get_comment_from_url_with_preload!(object_url)} - - "Document" -> - {:ok, Resources.get_resource_by_url_with_preloads(object_url)} - - "ResourceCollection" -> - {:ok, Resources.get_resource_by_url_with_preloads(object_url)} - - "Actor" -> - {:ok, Actors.get_actor_by_url!(object_url, true)} - - other -> - {:error, other} - end + {:existing, nil} <- + {:existing, Tombstone.find_tombstone(url)}, + {:existing, nil} <- {:existing, Events.get_event_by_url(url)}, + {:existing, nil} <- + {:existing, Discussions.get_discussion_by_url(url)}, + {:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)}, + {:existing, nil} <- {:existing, Resources.get_resource_by_url(url)}, + {:existing, nil} <- + {:existing, Actors.get_actor_by_url_2(url)}, + :ok <- Logger.info("Data for URL not found anywhere, going to fetch it"), + {:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do + Logger.debug("Going to preload the new entity") + Preloader.maybe_preload(entity) else - {:existing_event, %Event{url: event_url}} -> - {:ok, Events.get_public_event_by_url_with_preload!(event_url)} + {:existing, entity} -> + Logger.debug("Entity is already existing") - {:existing_comment, %Comment{url: comment_url}} -> - {:ok, Conversations.get_comment_from_url_with_preload!(comment_url)} + entity = + if force_fetch and not compare_origins?(url, Endpoint.url()) do + Logger.debug("Entity is external and we want a force fetch") - {:existing_resource, %Resource{url: resource_url}} -> - {:ok, Resources.get_resource_by_url_with_preloads(resource_url)} + with {:ok, _activity, entity} <- Fetcher.fetch_and_update(url, options) do + entity + end + else + entity + end - {:existing_actor, {:ok, %Actor{url: actor_url}}} -> - {:ok, Actors.get_actor_by_url!(actor_url, true)} + Logger.debug("Going to preload an existing entity") - {:origin_check, false} -> - Logger.warn("Object origin check failed") - {:error, "Object origin check failed"} + Preloader.maybe_preload(entity) e -> Logger.warn("Something failed while fetching url #{inspect(e)}") @@ -201,15 +167,18 @@ defmodule Mobilizon.Federation.ActivityPub do with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)}, {:ok, entity, create_data} <- (case type do - :event -> create_event(args, additional) - :comment -> create_comment(args, additional) - :group -> create_group(args, additional) - :todo_list -> create_todo_list(args, additional) - :todo -> create_todo(args, additional) - :resource -> create_resource(args, additional) + :event -> Types.Events.create(args, additional) + :comment -> Types.Comments.create(args, additional) + :discussion -> Types.Discussions.create(args, additional) + :actor -> Types.Actors.create(args, additional) + :todo_list -> Types.TodoLists.create(args, additional) + :todo -> Types.Todos.create(args, additional) + :resource -> Types.Resources.create(args, additional) + :post -> Types.Posts.create(args, additional) end), {:ok, activity} <- create_activity(create_data, local), - :ok <- maybe_federate(activity) do + :ok <- maybe_federate(activity), + :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, entity} else err -> @@ -227,21 +196,15 @@ defmodule Mobilizon.Federation.ActivityPub do * Federates (asynchronously) the activity * Returns the activity """ - @spec update(atom(), struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any() - def update(type, old_entity, args, local \\ false, additional \\ %{}) do + @spec update(struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any() + def update(old_entity, args, local \\ false, additional \\ %{}) do Logger.debug("updating an activity") Logger.debug(inspect(args)) - with {:ok, entity, update_data} <- - (case type do - :event -> update_event(old_entity, args, additional) - :comment -> update_comment(old_entity, args, additional) - :actor -> update_actor(old_entity, args, additional) - :todo -> update_todo(old_entity, args, additional) - :resource -> update_resource(old_entity, args, additional) - end), + with {:ok, entity, update_data} <- Managable.update(old_entity, args, additional), {:ok, activity} <- create_activity(update_data, local), - :ok <- maybe_federate(activity) do + :ok <- maybe_federate(activity), + :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, entity} else err -> @@ -366,182 +329,48 @@ defmodule Mobilizon.Federation.ActivityPub do end end - def delete(object, local \\ true) - - @spec delete(Event.t(), boolean) :: {:ok, Activity.t(), Event.t()} - def delete(%Event{url: url, organizer_actor: actor} = event, local) do - data = %{ - "type" => "Delete", - "actor" => actor.url, - "object" => url, - "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"], - "id" => url <> "/delete" - } - - with audience <- - Audience.calculate_to_and_cc_from_mentions(event), - {:ok, %Event{} = event} <- Events.delete_event(event), - {:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"), - {:ok, %Tombstone{} = _tombstone} <- - Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}), - Share.delete_all_by_uri(event.url), + def delete(object, actor, local \\ true) do + with {:ok, activity_data, actor, object} <- + Managable.delete(object, actor, local), + group <- Ownable.group_actor(object), :ok <- check_for_actor_key_rotation(actor), - {:ok, activity} <- create_activity(Map.merge(data, audience), local), - :ok <- maybe_federate(activity) do - {:ok, activity, event} + {:ok, activity} <- create_activity(activity_data, local), + :ok <- maybe_federate(activity), + :ok <- maybe_relay_if_group_activity(activity, group) do + {:ok, activity, object} end end - @spec delete(Comment.t(), boolean) :: {:ok, Activity.t(), Comment.t()} - def delete(%Comment{url: url, actor: actor} = comment, local) do - data = %{ - "type" => "Delete", - "actor" => actor.url, - "object" => url, - "id" => url <> "/delete", - "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] - } - - with audience <- - Audience.calculate_to_and_cc_from_mentions(comment), - {:ok, %Comment{} = comment} <- Conversations.delete_comment(comment), - {:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"), - {:ok, %Tombstone{} = _tombstone} <- - Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}), - Share.delete_all_by_uri(comment.url), - :ok <- check_for_actor_key_rotation(actor), - {:ok, activity} <- create_activity(Map.merge(data, audience), local), + def join(%Event{} = event, %Actor{} = actor, local \\ true, additional \\ %{}) do + with {:ok, activity_data, participant} <- Types.Events.join(event, actor, local, additional), + {:ok, activity} <- create_activity(activity_data, local), :ok <- maybe_federate(activity) do - {:ok, activity, comment} - end - end - - def delete(%Actor{url: url} = actor, local) do - data = %{ - "type" => "Delete", - "actor" => url, - "object" => url, - "id" => url <> "/delete", - "to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] - } - - # We completely delete the actor if activity is remote - with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor, reserve_username: local), - {:ok, activity} <- create_activity(data, local), - :ok <- maybe_federate(activity) do - {:ok, activity, actor} - end - end - - def delete( - %Resource{url: url, actor: %Actor{url: actor_url}} = resource, - local - ) do - Logger.debug("Building Delete Resource activity") - - data = %{ - "actor" => actor_url, - "type" => "Delete", - "object" => url, - "id" => url <> "/delete", - "to" => [actor_url] - } - - Logger.debug(inspect(data)) - - with {:ok, _resource} <- Resources.delete_resource(resource), - {:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}"), - {:ok, activity} <- create_activity(data, local), - :ok <- maybe_federate(activity) do - {:ok, activity, resource} - end - end - - def flag(args, local \\ false, _additional \\ %{}) do - with {:build_args, args} <- {:build_args, prepare_args_for_report(args)}, - {:create_report, {:ok, %Report{} = report}} <- - {:create_report, Reports.create_report(args)}, - report_as_data <- Convertible.model_to_as(report), - cc <- if(local, do: [report.reported.url], else: []), - report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}), - {:ok, activity} <- create_activity(report_as_data, local), - :ok <- maybe_federate(activity) do - Enum.each(Users.list_moderators(), fn moderator -> - moderator - |> Admin.report(report) - |> Mailer.deliver_later() - end) - - {:ok, activity, report} + {:ok, activity, participant} else - err -> - Logger.error("Something went wrong while creating an activity") - Logger.debug(inspect(err)) - err + {:maximum_attendee_capacity, err} -> + {:maximum_attendee_capacity, err} + + {:accept, accept} -> + accept end end - def join(object, actor, local \\ true, additional \\ %{}) - - def join(%Event{} = event, %Actor{} = actor, local, additional) do - # TODO Refactor me for federation - with {:maximum_attendee_capacity, true} <- - {:maximum_attendee_capacity, check_attendee_capacity(event)}, - role <- - additional - |> Map.get(:metadata, %{}) - |> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)), - {:ok, %Participant{} = participant} <- - Mobilizon.Events.create_participant(%{ - role: role, - event_id: event.id, - actor_id: actor.id, - url: Map.get(additional, :url), - metadata: - additional - |> Map.get(:metadata, %{}) - |> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1))) + def join_group( + %{parent_id: parent_id, actor_id: actor_id, role: role}, + local \\ true, + additional \\ %{} + ) do + with {:ok, %Member{} = member} <- + Mobilizon.Actors.create_member(%{ + parent_id: parent_id, + actor_id: actor_id, + role: role }), - join_data <- Convertible.model_to_as(participant), - audience <- - Audience.calculate_to_and_cc_from_mentions(participant), - {:ok, activity} <- create_activity(Map.merge(join_data, audience), local), + activity_data when is_map(activity_data) <- + Convertible.model_to_as(member), + {:ok, activity} <- create_activity(Map.merge(activity_data, additional), local), :ok <- maybe_federate(activity) do - if event.local do - cond do - Mobilizon.Events.get_default_participant_role(event) === :participant && - role == :participant -> - accept( - :join, - participant, - true, - %{"actor" => event.organizer_actor.url} - ) - - Mobilizon.Events.get_default_participant_role(event) === :not_approved && - role == :not_approved -> - Scheduler.pending_participation_notification(event) - {:ok, activity, participant} - - true -> - {:ok, activity, participant} - end - else - {:ok, activity, participant} - end - end - end - - # TODO: Implement me - def join(%Actor{type: :Group} = _group, %Actor{} = _actor, _local, _additional) do - :error - end - - defp check_attendee_capacity(%Event{options: options} = event) do - with maximum_attendee_capacity <- - Map.get(options, :maximum_attendee_capacity) || 0 do - maximum_attendee_capacity == 0 || - Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity + {:ok, activity, member} end end @@ -640,7 +469,7 @@ defmodule Mobilizon.Federation.ActivityPub do with {:ok, entity, update_data} <- (case type do - :resource -> move_resource(old_entity, args, additional) + :resource -> Types.Resources.move(old_entity, args, additional) end), {:ok, activity} <- create_activity(update_data, local), :ok <- maybe_federate(activity) do @@ -653,6 +482,25 @@ defmodule Mobilizon.Federation.ActivityPub do end end + def flag(args, local \\ false, additional \\ %{}) do + with {report, report_as_data} <- Types.Reports.flag(args, local, additional), + {:ok, activity} <- create_activity(report_as_data, local), + :ok <- maybe_federate(activity) do + Enum.each(Users.list_moderators(), fn moderator -> + moderator + |> Admin.report(report) + |> Mailer.deliver_later() + end) + + {:ok, activity, report} + else + err -> + Logger.error("Something went wrong while creating an activity") + Logger.debug(inspect(err)) + err + end + end + @doc """ Create an actor locally by its URL (AP ID) """ @@ -711,9 +559,29 @@ defmodule Mobilizon.Federation.ActivityPub do defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true defp is_create_activity?(_), do: false - @spec is_announce_activity?(Activity.t()) :: boolean - defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true - defp is_announce_activity?(_), do: false + @spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())} + defp convert_members_in_recipients(recipients) do + Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc -> + case Actors.get_group_by_members_url(recipient) do + # If the group is local just add external members + %Actor{domain: domain} = group when is_nil(domain) -> + {Enum.filter(recipients, fn recipient -> recipient != group.members_url end), + member_actors ++ Actors.list_external_actors_members_for_group(group)} + + # If it's remote add the remote group actor as well + %Actor{} = group -> + {Enum.filter(recipients, fn recipient -> recipient != group.members_url end), + member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]} + + _ -> + acc + end + end) + end + + # @spec is_announce_activity?(Activity.t()) :: boolean + # defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true + # defp is_announce_activity?(_), do: false @doc """ Publish an activity to all appropriated audiences inboxes @@ -741,19 +609,11 @@ defmodule Mobilizon.Federation.ActivityPub do {recipients, []} end - # If we want to send to all members of the group, because this server is the one the group is on - {recipients, members} = - if is_announce_activity?(activity) and actor.type == :Group and - actor.members_url in activity.recipients and is_nil(actor.domain) do - {Enum.filter(recipients, fn recipient -> recipient != actor.members_url end), - Actors.list_external_members_for_group(actor)} - else - {recipients, []} - end + {recipients, members} = convert_members_in_recipients(recipients) remote_inboxes = (remote_actors(recipients) ++ followers ++ members) - |> Enum.map(fn follower -> follower.shared_inbox_url || follower.inbox_url end) + |> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end) |> Enum.uniq() {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) @@ -791,16 +651,15 @@ defmodule Mobilizon.Federation.ActivityPub do date: date }) - HTTPoison.post( + Tesla.post( inbox, json, - [ + headers: [ {"Content-Type", "application/activity+json"}, {"signature", signature}, {"digest", digest}, {"date", date} - ], - hackney: [pool: :default] + ] ) end @@ -811,18 +670,15 @@ defmodule Mobilizon.Federation.ActivityPub do Logger.debug(inspect(url)) res = - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, [Accept: "application/activity+json"], - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ), + with {:ok, %{status: 200, body: body}} <- + Tesla.get(url, headers: [{"Accept", "application/activity+json"}]), :ok <- Logger.debug("response okay, now decoding json"), {:ok, data} <- Jason.decode(body) do Logger.debug("Got activity+json response at actor's endpoint, now converting data") {:ok, Converter.Actor.as_to_model_data(data)} else # Actor is gone, probably deleted - {:ok, %HTTPoison.Response{status_code: 410}} -> + {:ok, %{status: 410}} -> Logger.info("Response HTTP 410") {:error, :actor_deleted} @@ -839,10 +695,11 @@ defmodule Mobilizon.Federation.ActivityPub do """ @spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map() def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do - {:ok, events, total_events} = Events.list_public_events_for_actor(actor, page, limit) + %Page{total: total_events, elements: events} = + Events.list_public_events_for_actor(actor, page, limit) - {:ok, comments, total_comments} = - Conversations.list_public_comments_for_actor(actor, page, limit) + %Page{total: total_comments, elements: comments} = + Discussions.list_public_comments_for_actor(actor, page, limit) event_activities = Enum.map(events, &event_to_activity/1) comment_activities = Enum.map(comments, &comment_to_activity/1) @@ -879,252 +736,10 @@ defmodule Mobilizon.Federation.ActivityPub do Map.get(data, "to", []) ++ Map.get(data, "cc", []) end - @spec create_event(map(), map()) :: {:ok, map()} - defp create_event(args, additional) do - with args <- prepare_args_for_event(args), - {:ok, %Event{} = event} <- Events.create_event(args), - event_as_data <- Convertible.model_to_as(event), - audience <- - Audience.calculate_to_and_cc_from_mentions(event), - create_data <- - make_create_data(event_as_data, Map.merge(audience, additional)) do - {:ok, event, create_data} - end - end - - @spec create_comment(map(), map()) :: {:ok, map()} - defp create_comment(args, additional) do - with args <- prepare_args_for_comment(args), - {:ok, %Comment{} = comment} <- Conversations.create_comment(args), - comment_as_data <- Convertible.model_to_as(comment), - audience <- - Audience.calculate_to_and_cc_from_mentions(comment), - create_data <- - make_create_data(comment_as_data, Map.merge(audience, additional)) do - {:ok, comment, create_data} - end - end - - @spec create_group(map(), map()) :: {:ok, map()} - defp create_group(args, additional) do - with args <- prepare_args_for_group(args), - {:ok, %Actor{type: :Group} = group} <- Actors.create_group(args), - group_as_data <- Convertible.model_to_as(group), - audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []}, - create_data <- - make_create_data(group_as_data, Map.merge(audience, additional)) do - {:ok, group, create_data} - end - end - - @spec create_todo_list(map(), map()) :: {:ok, map()} - defp create_todo_list(args, additional) do - with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}), - audience <- %{"to" => [group.url], "cc" => []}, - create_data <- - make_create_data(todo_list_as_data, Map.merge(audience, additional)) do - {:ok, todo_list, create_data} - end - end - - @spec create_todo(map(), map()) :: {:ok, map()} - defp create_todo(args, additional) do - with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <- - Todos.create_todo(args), - %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id), - %Actor{} = creator <- Actors.get_actor(creator_id), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator}, - todo_as_data <- - Convertible.model_to_as(todo), - audience <- %{"to" => [group.url], "cc" => []}, - create_data <- - make_create_data(todo_as_data, Map.merge(audience, additional)) do - {:ok, todo, create_data} - end - end - - defp create_resource(%{type: type} = args, additional) do - args = - case type do - :folder -> - args - - _ -> - case Parser.parse(Map.get(args, :resource_url)) do - {:ok, metadata} -> - Map.put(args, :metadata, metadata) - - _ -> - args - end - end - - with {:ok, - %Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <- - Resources.create_resource(args), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), - resource_as_data <- - Convertible.model_to_as(%{resource | actor: group, creator: creator}), - audience <- %{ - "to" => [group.url], - "cc" => [], - "actor" => creator_url, - "attributedTo" => [creator_url] - } do - create_data = - case parent_id do - nil -> - make_create_data(resource_as_data, Map.merge(audience, additional)) - - parent_id -> - # In case the resource has a parent we don't `Create` the resource but `Add` it to an existing resource - parent = Resources.get_resource(parent_id) - make_add_data(resource_as_data, parent, Map.merge(audience, additional)) - end - - {:ok, resource, create_data} - else - err -> - Logger.error(inspect(err)) - err - end - end - @spec check_for_tombstones(map()) :: Tombstone.t() | nil defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url) defp check_for_tombstones(_), do: nil - @spec update_event(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any() - defp update_event(%Event{} = old_event, args, additional) do - with args <- prepare_args_for_event(args), - {:ok, %Event{} = new_event} <- Events.update_event(old_event, args), - {:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"), - event_as_data <- Convertible.model_to_as(new_event), - audience <- - Audience.calculate_to_and_cc_from_mentions(new_event), - update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do - {:ok, new_event, update_data} - else - err -> - Logger.error("Something went wrong while creating an update activity") - Logger.debug(inspect(err)) - err - end - end - - @spec update_comment(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any() - defp update_comment(%Comment{} = old_comment, args, additional) do - with args <- prepare_args_for_comment(args), - {:ok, %Comment{} = new_comment} <- Conversations.update_comment(old_comment, args), - {:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"), - comment_as_data <- Convertible.model_to_as(new_comment), - audience <- - Audience.calculate_to_and_cc_from_mentions(new_comment), - update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do - {:ok, new_comment, update_data} - else - err -> - Logger.error("Something went wrong while creating an update activity") - Logger.debug(inspect(err)) - err - end - end - - @spec update_actor(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any - defp update_actor(%Actor{} = old_actor, args, additional) do - with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args), - actor_as_data <- Convertible.model_to_as(new_actor), - {:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"), - audience <- - Audience.calculate_to_and_cc_from_mentions(new_actor), - additional <- Map.merge(additional, %{"actor" => old_actor.url}), - update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do - {:ok, new_actor, update_data} - end - end - - @spec update_todo(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any - defp update_todo(%Todo{} = old_todo, args, additional) do - with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args), - %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - todo_as_data <- - Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}), - audience <- %{"to" => [group.url], "cc" => []}, - update_data <- - make_update_data(todo_as_data, Map.merge(audience, additional)) do - {:ok, todo, update_data} - end - end - - defp update_resource(%Resource{} = old_resource, %{parent_id: _parent_id} = args, additional) do - move_resource(old_resource, args, additional) - end - - # Simple rename - defp update_resource(%Resource{} = old_resource, %{title: title} = _args, additional) do - with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <- - Resources.update_resource(old_resource, %{title: title}), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - %Actor{url: creator_url} <- Actors.get_actor(creator_id), - resource_as_data <- - Convertible.model_to_as(%{resource | actor: group}), - audience <- %{ - "to" => [group.url], - "cc" => [], - "actor" => creator_url, - "attributedTo" => [creator_url] - }, - update_data <- - make_update_data(resource_as_data, Map.merge(audience, additional)) do - {:ok, resource, update_data} - else - err -> - Logger.error(inspect(err)) - err - end - end - - defp move_resource( - %Resource{parent_id: old_parent_id} = old_resource, - %{parent_id: _new_parent_id} = args, - additional - ) do - with {:ok, - %Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} = - resource} <- - Resources.update_resource(old_resource, args), - old_parent <- Resources.get_resource(old_parent_id), - new_parent <- Resources.get_resource(new_parent_id), - {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - %Actor{url: creator_url} <- Actors.get_actor(creator_id), - resource_as_data <- - Convertible.model_to_as(%{resource | actor: group}), - audience <- %{ - "to" => [group.url], - "cc" => [], - "actor" => creator_url, - "attributedTo" => [creator_url] - }, - move_data <- - make_move_data( - resource_as_data, - old_parent, - new_parent, - Map.merge(audience, additional) - ) do - {:ok, resource, move_data} - else - err -> - Logger.error(inspect(err)) - err - end - end - @spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any defp accept_follow(%Follower{} = follower, additional) do with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}), @@ -1254,138 +869,4 @@ defmodule Mobilizon.Federation.ActivityPub do err end end - - # Prepare and sanitize arguments for events - defp prepare_args_for_event(args) do - # If title is not set: we are not updating it - args = - if Map.has_key?(args, :title) && !is_nil(args.title), - do: Map.update(args, :title, "", &String.trim/1), - else: args - - # If we've been given a description (we might not get one if updating) - # sanitize it, HTML it, and extract tags & mentions from it - args = - if Map.has_key?(args, :description) && !is_nil(args.description) do - {description, mentions, tags} = - APIUtils.make_content_html( - String.trim(args.description), - Map.get(args, :tags, []), - "text/html" - ) - - mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions) - - Map.merge(args, %{ - description: description, - mentions: mentions, - tags: tags - }) - else - args - end - - # Check that we can only allow anonymous participation if our instance allows it - {_, options} = - Map.get_and_update( - Map.get(args, :options, %{anonymous_participation: false}), - :anonymous_participation, - fn value -> - {value, value && Mobilizon.Config.anonymous_participation?()} - end - ) - - args = Map.put(args, :options, options) - - Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1) - end - - # Prepare and sanitize arguments for comments - defp prepare_args_for_comment(args) do - with in_reply_to_comment <- - args |> Map.get(:in_reply_to_comment_id) |> Conversations.get_comment_with_preload(), - event <- args |> Map.get(:event_id) |> handle_event_for_comment(), - args <- Map.update(args, :visibility, :public, & &1), - {text, mentions, tags} <- - APIUtils.make_content_html( - args |> Map.get(:text, "") |> String.trim(), - # Can't put additional tags on a comment - [], - "text/html" - ), - tags <- ConverterUtils.fetch_tags(tags), - mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions), - args <- - Map.merge(args, %{ - actor_id: Map.get(args, :actor_id), - text: text, - mentions: mentions, - tags: tags, - event: event, - in_reply_to_comment: in_reply_to_comment, - in_reply_to_comment_id: - if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)), - origin_comment_id: - if(is_nil(in_reply_to_comment), - do: nil, - else: Comment.get_thread_id(in_reply_to_comment) - ) - }) do - args - end - end - - @spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil - defp handle_event_for_comment(event_id) when not is_nil(event_id) do - case Events.get_event_with_preload(event_id) do - {:ok, %Event{} = event} -> event - {:error, :event_not_found} -> nil - end - end - - defp handle_event_for_comment(nil), do: nil - - defp prepare_args_for_group(args) do - with preferred_username <- - args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(), - summary <- args |> Map.get(:summary, "") |> String.trim(), - {summary, _mentions, _tags} <- - summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do - %{args | preferred_username: preferred_username, summary: summary} - end - end - - defp prepare_args_for_report(args) do - with {:reporter, %Actor{} = reporter_actor} <- - {:reporter, Actors.get_actor!(args.reporter_id)}, - {:reported, %Actor{} = reported_actor} <- - {:reported, Actors.get_actor!(args.reported_id)}, - content <- HTML.strip_tags(args.content), - event <- Conversations.get_comment(Map.get(args, :event_id)), - {:get_report_comments, comments} <- - {:get_report_comments, - Conversations.list_comments_by_actor_and_ids( - reported_actor.id, - Map.get(args, :comments_ids, []) - )} do - Map.merge(args, %{ - reporter: reporter_actor, - reported: reported_actor, - content: content, - event: event, - comments: comments - }) - end - end - - defp check_for_actor_key_rotation(%Actor{} = actor) do - if Actors.should_rotate_actor_key(actor) do - Actors.schedule_key_rotation( - actor, - Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay] - ) - end - - :ok - end end diff --git a/lib/federation/activity_pub/audience.ex b/lib/federation/activity_pub/audience.ex index aabaaf7d0..04449cb34 100644 --- a/lib/federation/activity_pub/audience.ex +++ b/lib/federation/activity_pub/audience.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do alias Mobilizon.Actors alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Share alias Mobilizon.Storage.Repo @@ -79,6 +79,14 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do def get_addressed_actors(mentioned_users, _), do: mentioned_users + def calculate_to_and_cc_from_mentions( + %Comment{discussion: %Discussion{actor_id: actor_id}} = _comment + ) do + with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do + %{"to" => [members_url], "cc" => []} + end + end + def calculate_to_and_cc_from_mentions(%Comment{} = comment) do with mentioned_actors <- Enum.map(comment.mentions, &process_mention/1), addressed_actors <- get_addressed_actors(mentioned_actors, nil), @@ -96,6 +104,28 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do end end + def calculate_to_and_cc_from_mentions(%Discussion{actor_id: actor_id}) do + with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do + %{"to" => [members_url], "cc" => []} + end + end + + def calculate_to_and_cc_from_mentions(%Event{ + attributed_to: %Actor{members_url: members_url}, + visibility: visibility + }) do + case visibility do + :public -> + %{"to" => [members_url, @ap_public], "cc" => []} + + :unlisted -> + %{"to" => [members_url], "cc" => [@ap_public]} + + :private -> + %{"to" => [members_url], "cc" => []} + end + end + def calculate_to_and_cc_from_mentions(%Event{} = event) do with mentioned_actors <- Enum.map(event.mentions, &process_mention/1), addressed_actors <- get_addressed_actors(mentioned_actors, nil), diff --git a/lib/federation/activity_pub/fetcher.ex b/lib/federation/activity_pub/fetcher.ex new file mode 100644 index 000000000..d74cebda6 --- /dev/null +++ b/lib/federation/activity_pub/fetcher.ex @@ -0,0 +1,74 @@ +defmodule Mobilizon.Federation.ActivityPub.Fetcher do + @moduledoc """ + Module to handle direct URL ActivityPub fetches to remote content + + If you need to first get cached data, see `Mobilizon.Federation.ActivityPub.fetch_object_from_url/2` + """ + require Logger + + alias Mobilizon.Federation.HTTPSignatures.Signature + alias Mobilizon.Federation.ActivityPub.{Relay, Transmogrifier} + alias Mobilizon.Service.HTTP.ActivityPub, as: ActivityPubClient + + import Mobilizon.Federation.ActivityPub.Utils, + only: [maybe_date_fetch: 2, sign_fetch: 4, origin_check?: 2] + + @spec fetch(String.t(), Keyword.t()) :: {:ok, map()} + def fetch(url, options \\ []) do + on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor()) + + with date <- Signature.generate_date_header(), + headers <- + [{:Accept, "application/activity+json"}] + |> maybe_date_fetch(date) + |> sign_fetch(on_behalf_of, url, date), + client <- + ActivityPubClient.client(headers: headers), + {:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <- + ActivityPubClient.get(client, url) do + {:ok, data} + end + end + + @spec fetch_and_create(String.t(), Keyword.t()) :: {:ok, map(), struct()} + def fetch_and_create(url, options \\ []) do + with {:ok, data} when is_map(data) <- fetch(url, options), + :ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"), + :ok <- Logger.debug(inspect(data)), + {:origin_check, true} <- {:origin_check, origin_check?(url, data)}, + params <- %{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["attributedTo"] || data["actor"], + "object" => data + } do + Transmogrifier.handle_incoming(params) + else + {:origin_check, false} -> + Logger.warn("Object origin check failed") + {:error, "Object origin check failed"} + end + end + + @spec fetch_and_update(String.t(), Keyword.t()) :: {:ok, map(), struct()} + def fetch_and_update(url, options \\ []) do + with {:ok, data} when is_map(data) <- fetch(url, options), + :ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"), + :ok <- Logger.debug(inspect(data)), + {:origin_check, true} <- {:origin_check, origin_check?(url, data)}, + params <- %{ + "type" => "Update", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["attributedTo"] || data["actor"], + "object" => data + } do + Transmogrifier.handle_incoming(params) + else + {:origin_check, false} -> + Logger.warn("Object origin check failed") + {:error, "Object origin check failed"} + end + end +end diff --git a/lib/federation/activity_pub/preloader.ex b/lib/federation/activity_pub/preloader.ex new file mode 100644 index 000000000..b964c5b07 --- /dev/null +++ b/lib/federation/activity_pub/preloader.ex @@ -0,0 +1,30 @@ +defmodule Mobilizon.Federation.ActivityPub.Preloader do + @moduledoc """ + Module to ensure entities are correctly preloaded + """ + + # TODO: Move me in a more appropriate place + alias Mobilizon.{Actors, Discussions, Events, Resources} + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions.{Comment, Discussion} + alias Mobilizon.Events.Event + alias Mobilizon.Resources.Resource + alias Mobilizon.Tombstone + + def maybe_preload(%Event{url: url}), + do: {:ok, Events.get_public_event_by_url_with_preload!(url)} + + def maybe_preload(%Comment{url: url}), + do: {:ok, Discussions.get_comment_from_url_with_preload!(url)} + + def maybe_preload(%Discussion{} = discussion), do: {:ok, discussion} + + def maybe_preload(%Resource{url: url}), + do: {:ok, Resources.get_resource_by_url_with_preloads(url)} + + def maybe_preload(%Actor{url: url}), do: {:ok, Actors.get_actor_by_url!(url, true)} + + def maybe_preload(%Tombstone{uri: _uri} = tombstone), do: {:ok, tombstone} + + def maybe_preload(other), do: {:error, other} +end diff --git a/lib/federation/activity_pub/refresher.ex b/lib/federation/activity_pub/refresher.ex index ef7209e3c..021742d9c 100644 --- a/lib/federation/activity_pub/refresher.ex +++ b/lib/federation/activity_pub/refresher.ex @@ -3,24 +3,31 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do Module that provides functions to explore and fetch collections on a group """ - alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityStream.Converter.Member, as: MemberConverter - alias Mobilizon.Federation.ActivityStream.Converter.Resource, as: ResourceConverter - alias Mobilizon.Federation.HTTPSignatures.Signature - alias Mobilizon.Resources + alias Mobilizon.Federation.ActivityPub.{Fetcher, Transmogrifier} require Logger - import Mobilizon.Federation.ActivityPub.Utils, - only: [maybe_date_fetch: 2, sign_fetch: 4] - @spec fetch_group(String.t(), Actor.t()) :: :ok def fetch_group(group_url, %Actor{} = on_behalf_of) do - with {:ok, %Actor{resources_url: resources_url, members_url: members_url}} <- + with {:ok, + %Actor{ + outbox_url: outbox_url, + resources_url: resources_url, + members_url: members_url, + posts_url: posts_url, + todos_url: todos_url, + discussions_url: discussions_url, + events_url: events_url + }} <- ActivityPub.get_or_fetch_actor_by_url(group_url) do + fetch_collection(outbox_url, on_behalf_of) fetch_collection(members_url, on_behalf_of) fetch_collection(resources_url, on_behalf_of) + fetch_collection(posts_url, on_behalf_of) + fetch_collection(todos_url, on_behalf_of) + fetch_collection(discussions_url, on_behalf_of) + fetch_collection(events_url, on_behalf_of) end end @@ -30,12 +37,28 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do Logger.debug("Fetching and preparing collection from url") Logger.debug(inspect(collection_url)) - with {:ok, data} <- fetch(collection_url, on_behalf_of) do + with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do Logger.debug("Fetch ok, passing to process_collection") process_collection(data, on_behalf_of) end end + @spec fetch_element(String.t(), Actor.t()) :: any() + def fetch_element(url, %Actor{} = on_behalf_of) do + with {:ok, data} <- Fetcher.fetch(url, on_behalf_of: on_behalf_of) do + case handling_element(data) do + {:ok, _activity, entity} -> + {:ok, entity} + + {:ok, entity} -> + {:ok, entity} + + err -> + {:error, err} + end + end + end + defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of) when type in ["OrderedCollection", "OrderedCollectionPage"] do Logger.debug( @@ -55,55 +78,26 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do when is_bitstring(first) do Logger.debug("OrderedCollection has a first property pointing to an URI") - with {:ok, data} <- fetch(first, on_behalf_of) do + with {:ok, data} <- Fetcher.fetch(first, on_behalf_of: on_behalf_of) do Logger.debug("Fetched the collection for first property") process_collection(data, on_behalf_of) end end - defp handling_element(%{"type" => "Member"} = data) do - Logger.debug("Handling Member element") + defp handling_element(data) when is_map(data) do + activity = %{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["actor"], + "attributedTo" => data["attributedTo"], + "object" => data + } - data - |> MemberConverter.as_to_model_data() - |> Actors.create_member() + Transmogrifier.handle_incoming(activity) end - defp handling_element(%{"type" => type} = data) - when type in ["Document", "ResourceCollection"] do - Logger.debug("Handling Resource element") - - data - |> ResourceConverter.as_to_model_data() - |> Resources.create_resource() - end - - defp fetch(url, %Actor{} = on_behalf_of) do - with date <- Signature.generate_date_header(), - headers <- - [{:Accept, "application/activity+json"}] - |> maybe_date_fetch(date) - |> sign_fetch(on_behalf_of, url, date), - %HTTPoison.Response{status_code: 200, body: body} <- - HTTPoison.get!(url, headers, - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ), - {:ok, data} <- - Jason.decode(body) do - {:ok, data} - else - # Actor is gone, probably deleted - {:ok, %HTTPoison.Response{status_code: 410}} -> - Logger.info("Response HTTP 410") - {:error, :actor_deleted} - - {:origin_check, false} -> - {:error, "Origin check failed"} - - e -> - Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}") - {:error, e} - end + defp handling_element(uri) when is_binary(uri) do + ActivityPub.fetch_object_from_url(uri) end end diff --git a/lib/federation/activity_pub/transmogrifier.ex b/lib/federation/activity_pub/transmogrifier.ex index 561a3203c..29338dc02 100644 --- a/lib/federation/activity_pub/transmogrifier.ex +++ b/lib/federation/activity_pub/transmogrifier.ex @@ -8,17 +8,19 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do A module to handle coding from internal to wire ActivityPub and back. """ - alias Mobilizon.{Actors, Conversations, Events, Resources, Todos} + alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos} alias Mobilizon.Actors.{Actor, Follower, Member} - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Posts.Post alias Mobilizon.Resources.Resource alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.{Activity, Utils} + alias Mobilizon.Federation.ActivityPub.{Activity, Relay, Utils} + alias Mobilizon.Federation.ActivityPub.Types.Ownable alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} - + alias Mobilizon.Tombstone alias Mobilizon.Web.Email.{Group, Participation} require Logger @@ -62,10 +64,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do with object_data when is_map(object_data) <- object |> Converter.Comment.as_to_model_data(), {:existing_comment, {:error, :comment_not_found}} <- - {:existing_comment, Conversations.get_comment_from_url_with_preload(object_data.url)}, - {:ok, %Activity{} = activity, %Comment{} = comment} <- - ActivityPub.create(:comment, object_data, false) do - {:ok, activity, comment} + {:existing_comment, Discussions.get_comment_from_url_with_preload(object_data.url)}, + object_data <- transform_object_data_for_discussion(object_data) do + # Check should be better + + {:ok, %Activity{} = activity, entity} = + if is_data_for_comment_or_discussion?(object_data) do + Logger.debug("Chosing to create a regular comment") + ActivityPub.create(:comment, object_data, false) + else + Logger.debug("Chosing to initialize or add a comment to a conversation") + ActivityPub.create(:discussion, object_data, false) + end + + {:ok, activity, entity} else {:existing_comment, {:ok, %Comment{} = comment}} -> {:ok, nil, comment} @@ -100,6 +112,77 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do end end + def handle_incoming(%{ + "type" => "Create", + "object" => %{"type" => "Group", "id" => group_url} = _object + }) do + Logger.info("Handle incoming to create a group") + + with {:ok, %Actor{} = group} <- ActivityPub.get_or_fetch_actor_by_url(group_url) do + {:ok, nil, group} + end + end + + def handle_incoming(%{ + "type" => "Create", + "object" => %{"type" => "Member"} = object + }) do + Logger.info("Handle incoming to create a member") + + with object_data when is_map(object_data) <- + object |> Converter.Member.as_to_model_data(), + {:existing_member, nil} <- + {:existing_member, Actors.get_member_by_url(object_data.url)}, + {:ok, %Activity{} = activity, %Member{} = member} <- + ActivityPub.join_group(object_data, false) do + {:ok, activity, member} + else + {:existing_member, %Member{} = member} -> + {:ok, nil, member} + end + end + + def handle_incoming(%{ + "type" => "Create", + "object" => + %{"type" => "Article", "actor" => _actor, "attributedTo" => _attributed_to} = object + }) do + Logger.info("Handle incoming to create articles") + + with object_data when is_map(object_data) <- + object |> Converter.Post.as_to_model_data(), + {:existing_post, nil} <- + {:existing_post, Posts.get_post_by_url(object_data.url)}, + {:ok, %Activity{} = activity, %Post{} = post} <- + ActivityPub.create(:post, object_data, false) do + {:ok, activity, post} + else + {:existing_post, %Post{} = post} -> + {:ok, nil, post} + end + end + + # This is a hack to handle Tombstones fetched by AP + def handle_incoming(%{ + "type" => "Create", + "object" => %{"type" => "Tombstone", "id" => object_url} = _object + }) do + Logger.info("Handle incoming to create a tombstone") + + case ActivityPub.fetch_object_from_url(object_url, force: true) do + # We already have the tombstone, object is probably already deleted + {:ok, %Tombstone{} = tombstone} -> + {:ok, nil, tombstone} + + # Hack because deleted comments + {:ok, %Comment{deleted_at: deleted_at} = comment} when not is_nil(deleted_at) -> + {:ok, nil, comment} + + {:ok, entity} -> + ActivityPub.delete(entity, Relay.get_actor(), false) + end + end + def handle_incoming( %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data ) do @@ -165,7 +248,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do Logger.info("Handle incoming to create a resource") Logger.debug(inspect(data)) - group_url = hd(to) + group_url = if is_list(to) and not is_nil(to), do: hd(to), else: to with {:existing_resource, nil} <- {:existing_resource, Resources.get_resource_by_url(object_url)}, @@ -175,8 +258,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do {:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)}, {:ok, %Activity{} = activity, %Resource{} = resource} <- ActivityPub.create(:resource, object_data, false), - {:ok, %Actor{type: :Group, id: group_id} = group} <- - ActivityPub.get_or_fetch_actor_by_url(group_url), + %Actor{type: :Group, id: group_id} = group <- + Actors.get_group_by_members_url(group_url), announce_id <- "#{object_url}/announces/#{group_id}", {:ok, _activity, _resource} <- ActivityPub.announce(group, object, announce_id) do {:ok, activity, resource} @@ -190,7 +273,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do :error {:error, e} -> - Logger.error(inspect(e)) + Logger.debug(inspect(e)) :error end end @@ -261,23 +344,14 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Announce", "object" => object, "actor" => _actor, "id" => _id} = data ) do - with actor <- Utils.get_actor(data), - # TODO: Is the following line useful? - {:ok, %Actor{id: actor_id, suspended: false} = _actor} <- - ActivityPub.get_or_fetch_actor_by_url(actor), + with actor_url <- Utils.get_actor(data), + {:ok, %Actor{id: actor_id, suspended: false} = actor} <- + ActivityPub.get_or_fetch_actor_by_url(actor_url), :ok <- Logger.debug("Fetching contained object"), - {:ok, object} <- fetch_obj_helper_as_activity_streams(object), - :ok <- Logger.debug("Handling contained object"), - create_data <- Utils.make_create_data(object), - :ok <- Logger.debug(inspect(object)), - {:ok, _activity, entity} <- handle_incoming(create_data), - :ok <- Logger.debug("Finished processing contained object"), - {:ok, activity} <- ActivityPub.create_activity(data, false), - {:ok, %Actor{id: object_owner_actor_id}} <- - ActivityPub.get_or_fetch_actor_by_url(object["actor"]), - {:ok, %Mobilizon.Share{} = _share} <- - Mobilizon.Share.create(object["id"], actor_id, object_owner_actor_id) do - {:ok, activity, entity} + {:ok, entity} <- + object |> Utils.get_url() |> fetch_object_optionnally_authenticated(actor), + :ok <- eventually_create_share(object, entity, actor_id) do + {:ok, nil, entity} else e -> Logger.debug(inspect(e)) @@ -296,7 +370,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do object_data <- object |> Converter.Actor.as_to_model_data(), {:ok, %Activity{} = activity, %Actor{} = new_actor} <- - ActivityPub.update(:actor, old_actor, object_data, false) do + ActivityPub.update(old_actor, object_data, false) do {:ok, activity, new_actor} else e -> @@ -317,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do object_data <- Converter.Event.as_to_model_data(object), {:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)}, {:ok, %Activity{} = activity, %Event{} = new_event} <- - ActivityPub.update(:event, old_event, object_data, false) do + ActivityPub.update(old_event, object_data, false) do {:ok, activity, new_event} else _e -> @@ -325,6 +399,42 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do end end + def handle_incoming( + %{"type" => "Update", "object" => %{"type" => "Note"} = object, "actor" => _actor} = + update_data + ) do + with actor <- Utils.get_actor(update_data), + {:ok, %Actor{url: actor_url, suspended: false}} <- + ActivityPub.get_or_fetch_actor_by_url(actor), + {:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)}, + object_data <- Converter.Comment.as_to_model_data(object), + {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), + object_data <- transform_object_data_for_discussion(object_data), + {:ok, %Activity{} = activity, new_entity} <- + ActivityPub.update(old_entity, object_data, false) do + {:ok, activity, new_entity} + else + _e -> + :error + end + end + + def handle_incoming(%{ + "type" => "Update", + "object" => %{"type" => "Tombstone"} = object, + "actor" => _actor + }) do + Logger.info("Handle incoming to update a tombstone") + + with object_url <- Utils.get_url(object), + {:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do + ActivityPub.delete(entity, Relay.get_actor(), false) + else + {:ok, %Tombstone{} = tombstone} -> + {:ok, nil, tombstone} + end + end + def handle_incoming( %{ "type" => "Undo", @@ -367,21 +477,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do end end - # TODO: We presently assume that any actor on the same origin domain as the object being - # deleted has the rights to delete that object. A better way to validate whether or not - # the object should be deleted is to refetch the object URI, which should return either - # an error or a tombstone. This would allow us to verify that a deletion actually took - # place. + # We assume everyone on the same instance as the object + # or who is member of a group has the right to delete the object def handle_incoming( %{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data ) do - with actor <- Utils.get_actor(data), - {:ok, %Actor{url: actor_url}} <- ActivityPub.get_or_fetch_actor_by_url(actor), + with actor_url <- Utils.get_actor(data), + {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url), object_id <- Utils.get_url(object), - {:origin_check, true} <- - {:origin_check, Utils.origin_check_from_id?(actor_url, object_id)}, {:ok, object} <- ActivityPub.fetch_object_from_url(object_id), - {:ok, activity, object} <- ActivityPub.delete(object, false) do + {:origin_check, true} <- + {:origin_check, + Utils.origin_check_from_id?(actor_url, object_id) || + Utils.activity_actor_is_group_member?(actor, object)}, + {:ok, activity, object} <- ActivityPub.delete(object, actor, false) do {:ok, activity, object} else {:origin_check, false} -> @@ -449,6 +558,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do "target" => target } = data ) do + Logger.info("Handle incoming to invite someone") + with {:ok, %Actor{} = actor} <- data |> Utils.get_actor() |> ActivityPub.get_or_fetch_actor_by_url(), {:ok, object} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), @@ -485,7 +596,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do # end def handle_incoming(object) do - Logger.info("Handing something not supported") + Logger.info("Handing something with type #{object["type"]} not supported") Logger.debug(inspect(object)) {:error, :not_supported} end @@ -657,6 +768,52 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do end end + # If the object has been announced by a group let's use one of our members to fetch it + @spec fetch_object_optionnally_authenticated(String.t(), Actor.t() | any()) :: + {:ok, struct()} | {:error, any()} + defp fetch_object_optionnally_authenticated(url, %Actor{type: :Group, id: group_id}) do + case Actors.get_single_group_member_actor(group_id) do + %Actor{} = actor -> + ActivityPub.fetch_object_from_url(url, on_behalf_of: actor, force: true) + + _err -> + fetch_object_optionnally_authenticated(url, nil) + end + end + + defp fetch_object_optionnally_authenticated(url, _), + do: ActivityPub.fetch_object_from_url(url, force: true) + + defp eventually_create_share(object, entity, actor_id) do + with object_id <- object |> Utils.get_url(), + %Actor{id: object_owner_actor_id} <- Ownable.actor(entity) do + {:ok, %Mobilizon.Share{} = _share} = + Mobilizon.Share.create(object_id, actor_id, object_owner_actor_id) + end + + :ok + end + + @spec is_data_for_comment_or_discussion?(map()) :: boolean() + defp is_data_for_comment_or_discussion?(object_data) do + (not Map.has_key?(object_data, :title) or + is_nil(object_data.title) or object_data.title == "") and + is_nil(object_data.discussion_id) + end + + # Comment and conversations have different attributes for actor and groups + defp transform_object_data_for_discussion(object_data) do + # Basic comment + if is_data_for_comment_or_discussion?(object_data) do + object_data + else + # Conversation + object_data + |> Map.put(:creator_id, object_data.actor_id) + |> Map.put(:actor_id, object_data.attributed_to_id) + end + end + defp get_follow(follow_object) do with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object), {:not_found, %Follower{} = follow} <- diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex new file mode 100644 index 000000000..c9360959e --- /dev/null +++ b/lib/federation/activity_pub/types/actors.ex @@ -0,0 +1,74 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Actors do + @moduledoc false + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub.Audience + alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.GraphQL.API.Utils, as: APIUtils + alias Mobilizon.Service.Formatter.HTML + import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] + + @behaviour Entity + + @impl Entity + @spec create(map(), map()) :: {:ok, map()} + def create(args, additional) do + with args <- prepare_args_for_actor(args), + {:ok, %Actor{} = actor} <- Actors.create_actor(args), + actor_as_data <- Convertible.model_to_as(actor), + audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []}, + create_data <- + make_create_data(actor_as_data, Map.merge(audience, additional)) do + {:ok, actor, create_data} + end + end + + @impl Entity + @spec update(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any + def update(%Actor{} = old_actor, args, additional) do + with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args), + actor_as_data <- Convertible.model_to_as(new_actor), + {:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"), + audience <- + Audience.calculate_to_and_cc_from_mentions(new_actor), + additional <- Map.merge(additional, %{"actor" => old_actor.url}), + update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do + {:ok, new_actor, update_data} + end + end + + @impl Entity + def delete( + %Actor{followers_url: followers_url, url: target_actor_url} = target_actor, + %Actor{url: actor_url} = actor, + local + ) do + activity_data = %{ + "type" => "Delete", + "actor" => actor_url, + "object" => Convertible.model_to_as(target_actor), + "id" => target_actor_url <> "/delete", + "to" => [followers_url, "https://www.w3.org/ns/activitystreams#Public"] + } + + # We completely delete the actor if activity is remote + with {:ok, %Oban.Job{}} <- Actors.delete_actor(target_actor, reserve_username: local) do + {:ok, activity_data, actor, target_actor} + end + end + + def actor(%Actor{} = actor), do: actor + + def group_actor(%Actor{} = _actor), do: nil + + defp prepare_args_for_actor(args) do + with preferred_username <- + args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(), + summary <- args |> Map.get(:summary, "") |> String.trim(), + {summary, _mentions, _tags} <- + summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do + %{args | preferred_username: preferred_username, summary: summary} + end + end +end diff --git a/lib/federation/activity_pub/types/comments.ex b/lib/federation/activity_pub/types/comments.ex new file mode 100644 index 000000000..5c1ce153c --- /dev/null +++ b/lib/federation/activity_pub/types/comments.ex @@ -0,0 +1,149 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Comments do + @moduledoc false + alias Mobilizon.{Actors, Discussions, Events} + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions.{Comment, Discussion} + alias Mobilizon.Events.Event + alias Mobilizon.Federation.ActivityPub.Audience + alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.GraphQL.API.Utils, as: APIUtils + alias Mobilizon.Share + alias Mobilizon.Tombstone + alias Mobilizon.Web.Endpoint + import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] + require Logger + + @behaviour Entity + + @impl Entity + @spec create(map(), map()) :: {:ok, map()} + def create(args, additional) do + with args <- prepare_args_for_comment(args), + {:ok, %Comment{discussion_id: discussion_id} = comment} <- + Discussions.create_comment(args), + :ok <- maybe_publish_graphql_subscription(discussion_id), + comment_as_data <- Convertible.model_to_as(comment), + audience <- + Audience.calculate_to_and_cc_from_mentions(comment), + create_data <- + make_create_data(comment_as_data, Map.merge(audience, additional)) do + {:ok, comment, create_data} + end + end + + @impl Entity + @spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any() + def update(%Comment{} = old_comment, args, additional) do + with args <- prepare_args_for_comment(args), + {:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args), + {:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"), + comment_as_data <- Convertible.model_to_as(new_comment), + audience <- + Audience.calculate_to_and_cc_from_mentions(new_comment), + update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do + {:ok, new_comment, update_data} + else + err -> + Logger.error("Something went wrong while creating an update activity") + Logger.debug(inspect(err)) + err + end + end + + @impl Entity + @spec delete(Comment.t(), Actor.t(), boolean) :: {:ok, Comment.t()} + def delete(%Comment{url: url} = comment, %Actor{} = actor, _local) do + activity_data = %{ + "type" => "Delete", + "actor" => actor.url, + "object" => Convertible.model_to_as(comment), + "id" => url <> "/delete", + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + } + + with audience <- + Audience.calculate_to_and_cc_from_mentions(comment), + {:ok, %Comment{} = comment} <- Discussions.delete_comment(comment), + # Preload to be sure + %Comment{} = comment <- Discussions.get_comment_with_preload(comment.id), + {:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"), + {:ok, %Tombstone{} = _tombstone} <- + Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do + Share.delete_all_by_uri(comment.url) + {:ok, Map.merge(activity_data, audience), actor, comment} + end + end + + def actor(%Comment{actor: %Actor{} = actor}), do: actor + + def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id), + do: Actors.get_actor(actor_id) + + def actor(_), do: nil + + def group_actor(%Comment{attributed_to: %Actor{} = group}), do: group + + def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), + do: Actors.get_actor(attributed_to_id) + + def group_actor(_), do: nil + + # Prepare and sanitize arguments for comments + defp prepare_args_for_comment(args) do + with in_reply_to_comment <- + args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(), + event <- args |> Map.get(:event_id) |> handle_event_for_comment(), + args <- Map.update(args, :visibility, :public, & &1), + {text, mentions, tags} <- + APIUtils.make_content_html( + args |> Map.get(:text, "") |> String.trim(), + # Can't put additional tags on a comment + [], + "text/html" + ), + tags <- ConverterUtils.fetch_tags(tags), + mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions), + args <- + Map.merge(args, %{ + actor_id: Map.get(args, :actor_id), + text: text, + mentions: mentions, + tags: tags, + event: event, + in_reply_to_comment: in_reply_to_comment, + in_reply_to_comment_id: + if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)), + origin_comment_id: + if(is_nil(in_reply_to_comment), + do: nil, + else: Comment.get_thread_id(in_reply_to_comment) + ) + }) do + args + end + end + + @spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil + defp handle_event_for_comment(event_id) when not is_nil(event_id) do + case Events.get_event_with_preload(event_id) do + {:ok, %Event{} = event} -> event + {:error, :event_not_found} -> nil + end + end + + defp handle_event_for_comment(nil), do: nil + + defp maybe_publish_graphql_subscription(nil), do: :ok + + defp maybe_publish_graphql_subscription(discussion_id) do + with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id) do + Absinthe.Subscription.publish(Endpoint, discussion, + discussion_comment_changed: discussion.slug + ) + + :ok + end + end +end diff --git a/lib/federation/activity_pub/types/discussions.ex b/lib/federation/activity_pub/types/discussions.ex new file mode 100644 index 000000000..a27d3c2cf --- /dev/null +++ b/lib/federation/activity_pub/types/discussions.ex @@ -0,0 +1,115 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do + @moduledoc false + + alias Mobilizon.{Actors, Discussions} + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions.{Comment, Discussion} + alias Mobilizon.Federation.ActivityPub.Audience + alias Mobilizon.Federation.ActivityPub.Types.{Comments, Entity} + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Storage.Repo + alias Mobilizon.Web.Endpoint + import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] + require Logger + + @behaviour Entity + + @impl Entity + @spec create(map(), map()) :: {:ok, map()} + def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do + with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id), + {:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <- + Discussions.reply_to_discussion(discussion, args), + %Comment{} = last_comment <- Discussions.get_comment_with_preload(last_comment_id), + :ok <- maybe_publish_graphql_subscription(discussion), + comment_as_data <- Convertible.model_to_as(last_comment), + audience <- + Audience.calculate_to_and_cc_from_mentions(discussion), + create_data <- + make_create_data(comment_as_data, Map.merge(audience, additional)) do + {:ok, discussion, create_data} + end + end + + @impl Entity + @spec create(map(), map()) :: {:ok, map()} + def create(args, additional) do + with {:ok, %Discussion{} = discussion} <- + Discussions.create_discussion(args), + discussion_as_data <- Convertible.model_to_as(discussion), + audience <- + Audience.calculate_to_and_cc_from_mentions(discussion), + create_data <- + make_create_data(discussion_as_data, Map.merge(audience, additional)) do + {:ok, discussion, create_data} + end + end + + @impl Entity + @spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), Activity.t()} | any() + def update(%Discussion{} = old_discussion, args, additional) do + with {:ok, %Discussion{} = new_discussion} <- + Discussions.update_discussion(old_discussion, args), + {:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"), + discussion_as_data <- Convertible.model_to_as(new_discussion), + audience <- + Audience.calculate_to_and_cc_from_mentions(new_discussion), + update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do + {:ok, new_discussion, update_data} + else + err -> + Logger.error("Something went wrong while creating an update activity") + Logger.debug(inspect(err)) + err + end + end + + @impl Entity + @spec delete(Discussion.t(), Actor.t(), boolean) :: {:ok, Discussion.t()} + def delete(%Discussion{actor: group, url: url} = discussion, %Actor{} = actor, _local) do + stream = + discussion.comments + |> Enum.map( + &Repo.preload(&1, [ + :actor, + :attributed_to, + :in_reply_to_comment, + :mentions, + :origin_comment, + :discussion, + :tags, + :replies + ]) + ) + |> Enum.map(&Map.put(&1, :event, nil)) + |> Task.async_stream(fn comment -> Comments.delete(comment, actor, nil) end) + + Stream.run(stream) + + with {:ok, %Discussion{}} <- Discussions.delete_discussion(discussion) do + # This is just fake + activity_data = %{ + "type" => "Delete", + "actor" => actor.url, + "object" => Convertible.model_to_as(discussion), + "id" => url <> "/delete", + "to" => [group.members_url] + } + + {:ok, activity_data, actor, discussion} + end + end + + def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id) + + def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id) + + @spec maybe_publish_graphql_subscription(Discussion.t()) :: :ok + defp maybe_publish_graphql_subscription(%Discussion{} = discussion) do + Absinthe.Subscription.publish(Endpoint, discussion, + discussion_comment_changed: discussion.slug + ) + + :ok + end +end diff --git a/lib/federation/activity_pub/types/entity.ex b/lib/federation/activity_pub/types/entity.ex new file mode 100644 index 000000000..f27565517 --- /dev/null +++ b/lib/federation/activity_pub/types/entity.ex @@ -0,0 +1,151 @@ +alias Mobilizon.Federation.ActivityPub.Types.{ + Actors, + Comments, + Discussions, + Entity, + Events, + Managable, + Ownable, + Posts, + Resources, + Todos, + TodoLists, + Tombstones +} + +alias Mobilizon.Actors.Actor +alias Mobilizon.Events.Event +alias Mobilizon.Discussions.{Comment, Discussion} +alias Mobilizon.Posts.Post +alias Mobilizon.Resources.Resource +alias Mobilizon.Todos.{Todo, TodoList} +alias Mobilizon.Federation.ActivityStream +alias Mobilizon.Tombstone + +defmodule Mobilizon.Federation.ActivityPub.Types.Entity do + @moduledoc """ + ActivityPub entity behaviour + """ + @type t :: %{id: String.t()} + + @callback create(data :: any(), additionnal :: map()) :: + {:ok, t(), ActivityStream.t()} + + @callback update(struct :: t(), attrs :: map(), additionnal :: map()) :: + {:ok, t(), ActivityStream.t()} + + @callback delete(struct :: t(), Actor.t(), local :: boolean()) :: + {:ok, ActivityStream.t(), Actor.t(), t()} +end + +defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do + @moduledoc """ + ActivityPub entity Managable protocol. + """ + + @spec update(Entity.t(), map(), map()) :: {:ok, Entity.t(), ActivityStream.t()} + @doc """ + Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it + """ + def update(entity, attrs, additionnal) + + @spec delete(Entity.t(), Actor.t(), boolean()) :: + {:ok, ActivityStream.t(), Actor.t(), Entity.t()} + @doc "Deletes an entity and returns the activitystream representation for it" + def delete(entity, actor, local) +end + +defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do + @spec group_actor(Entity.t()) :: Actor.t() | nil + @doc "Returns an eventual group for the entity" + def group_actor(entity) + + @spec actor(Entity.t()) :: Actor.t() | nil + @doc "Returns the actor for the entity" + def actor(entity) +end + +defimpl Managable, for: Event do + defdelegate update(entity, attrs, additionnal), to: Events + defdelegate delete(entity, actor, local), to: Events +end + +defimpl Ownable, for: Event do + defdelegate group_actor(entity), to: Events + defdelegate actor(entity), to: Events +end + +defimpl Managable, for: Comment do + defdelegate update(entity, attrs, additionnal), to: Comments + defdelegate delete(entity, actor, local), to: Comments +end + +defimpl Ownable, for: Comment do + defdelegate group_actor(entity), to: Comments + defdelegate actor(entity), to: Comments +end + +defimpl Managable, for: Post do + defdelegate update(entity, attrs, additionnal), to: Posts + defdelegate delete(entity, actor, local), to: Posts +end + +defimpl Ownable, for: Post do + defdelegate group_actor(entity), to: Posts + defdelegate actor(entity), to: Posts +end + +defimpl Managable, for: Actor do + defdelegate update(entity, attrs, additionnal), to: Actors + defdelegate delete(entity, actor, local), to: Actors +end + +defimpl Ownable, for: Actor do + defdelegate group_actor(entity), to: Actors + defdelegate actor(entity), to: Actors +end + +defimpl Managable, for: TodoList do + defdelegate update(entity, attrs, additionnal), to: TodoLists + defdelegate delete(entity, actor, local), to: TodoLists +end + +defimpl Ownable, for: TodoList do + defdelegate group_actor(entity), to: TodoLists + defdelegate actor(entity), to: TodoLists +end + +defimpl Managable, for: Todo do + defdelegate update(entity, attrs, additionnal), to: Todos + defdelegate delete(entity, actor, local), to: Todos +end + +defimpl Ownable, for: Todo do + defdelegate group_actor(entity), to: Todos + defdelegate actor(entity), to: Todos +end + +defimpl Managable, for: Resource do + defdelegate update(entity, attrs, additionnal), to: Resources + defdelegate delete(entity, actor, local), to: Resources +end + +defimpl Ownable, for: Resource do + defdelegate group_actor(entity), to: Resources + defdelegate actor(entity), to: Resources +end + +defimpl Managable, for: Discussion do + defdelegate update(entity, attrs, additionnal), to: Discussions + defdelegate delete(entity, actor, local), to: Discussions +end + +defimpl Ownable, for: Discussion do + defdelegate group_actor(entity), to: Discussions + defdelegate actor(entity), to: Discussions +end + +defimpl Ownable, for: Tombstone do + defdelegate group_actor(entity), to: Tombstones + defdelegate actor(entity), to: Tombstones +end diff --git a/lib/federation/activity_pub/types/events.ex b/lib/federation/activity_pub/types/events.ex new file mode 100644 index 000000000..ea9082661 --- /dev/null +++ b/lib/federation/activity_pub/types/events.ex @@ -0,0 +1,203 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Events do + @moduledoc false + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor + alias Mobilizon.Events, as: EventsManager + alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Audience + alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.GraphQL.API.Utils, as: APIUtils + alias Mobilizon.Service.Formatter.HTML + alias Mobilizon.Service.Notifications.Scheduler + alias Mobilizon.Share + alias Mobilizon.Tombstone + import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] + require Logger + + @behaviour Entity + + @impl Entity + @spec create(map(), map()) :: {:ok, map()} + def create(args, additional) do + with args <- prepare_args_for_event(args), + {:ok, %Event{} = event} <- EventsManager.create_event(args), + event_as_data <- Convertible.model_to_as(event), + audience <- + Audience.calculate_to_and_cc_from_mentions(event), + create_data <- + make_create_data(event_as_data, Map.merge(audience, additional)) do + {:ok, event, create_data} + end + end + + @impl Entity + @spec update(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any() + def update(%Event{} = old_event, args, additional) do + with args <- prepare_args_for_event(args), + {:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args), + {:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"), + event_as_data <- Convertible.model_to_as(new_event), + audience <- + Audience.calculate_to_and_cc_from_mentions(new_event), + update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do + {:ok, new_event, update_data} + else + err -> + Logger.error("Something went wrong while creating an update activity") + Logger.debug(inspect(err)) + err + end + end + + @impl Entity + @spec delete(Event.t(), Actor.t(), boolean) :: {:ok, Event.t()} + def delete(%Event{url: url} = event, %Actor{} = actor, _local) do + activity_data = %{ + "type" => "Delete", + "actor" => actor.url, + "object" => Convertible.model_to_as(event), + "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"], + "id" => url <> "/delete" + } + + with audience <- + Audience.calculate_to_and_cc_from_mentions(event), + {:ok, %Event{} = event} <- EventsManager.delete_event(event), + {:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"), + {:ok, %Tombstone{} = _tombstone} <- + Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do + Share.delete_all_by_uri(event.url) + {:ok, Map.merge(activity_data, audience), actor, event} + end + end + + def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor + + def actor(%Event{organizer_actor_id: organizer_actor_id}), + do: Actors.get_actor(organizer_actor_id) + + def actor(_), do: nil + + def group_actor(%Event{attributed_to: %Actor{} = group}), do: group + + def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), + do: Actors.get_actor(attributed_to_id) + + def group_actor(_), do: nil + + def join(%Event{} = event, %Actor{} = actor, _local, additional) do + with {:maximum_attendee_capacity, true} <- + {:maximum_attendee_capacity, check_attendee_capacity(event)}, + role <- + additional + |> Map.get(:metadata, %{}) + |> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)), + {:ok, %Participant{} = participant} <- + Mobilizon.Events.create_participant(%{ + role: role, + event_id: event.id, + actor_id: actor.id, + url: Map.get(additional, :url), + metadata: + additional + |> Map.get(:metadata, %{}) + |> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1))) + }), + join_data <- Convertible.model_to_as(participant), + audience <- + Audience.calculate_to_and_cc_from_mentions(participant) do + approve_if_default_role_is_participant( + event, + Map.merge(join_data, audience), + participant, + role + ) + else + {:maximum_attendee_capacity, err} -> + {:maximum_attendee_capacity, err} + end + end + + defp check_attendee_capacity(%Event{options: options} = event) do + with maximum_attendee_capacity <- + Map.get(options, :maximum_attendee_capacity) || 0 do + maximum_attendee_capacity == 0 || + Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity + end + end + + # Set the participant to approved if the default role for new participants is :participant + defp approve_if_default_role_is_participant(event, activity_data, participant, role) do + if event.local do + cond do + Mobilizon.Events.get_default_participant_role(event) === :participant && + role == :participant -> + {:accept, + ActivityPub.accept( + :join, + participant, + true, + %{"actor" => event.organizer_actor.url} + )} + + Mobilizon.Events.get_default_participant_role(event) === :not_approved && + role == :not_approved -> + Scheduler.pending_participation_notification(event) + {:ok, activity_data, participant} + + true -> + {:ok, activity_data, participant} + end + else + {:ok, activity_data, participant} + end + end + + # Prepare and sanitize arguments for events + defp prepare_args_for_event(args) do + # If title is not set: we are not updating it + args = + if Map.has_key?(args, :title) && !is_nil(args.title), + do: Map.update(args, :title, "", &String.trim/1), + else: args + + # If we've been given a description (we might not get one if updating) + # sanitize it, HTML it, and extract tags & mentions from it + args = + if Map.has_key?(args, :description) && !is_nil(args.description) do + {description, mentions, tags} = + APIUtils.make_content_html( + String.trim(args.description), + Map.get(args, :tags, []), + "text/html" + ) + + mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions) + + Map.merge(args, %{ + description: description, + mentions: mentions, + tags: tags + }) + else + args + end + + # Check that we can only allow anonymous participation if our instance allows it + {_, options} = + Map.get_and_update( + Map.get(args, :options, %{anonymous_participation: false}), + :anonymous_participation, + fn value -> + {value, value && Mobilizon.Config.anonymous_participation?()} + end + ) + + args = Map.put(args, :options, options) + + Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1) + end +end diff --git a/lib/federation/activity_pub/types/posts.ex b/lib/federation/activity_pub/types/posts.ex new file mode 100644 index 000000000..51f3aadf2 --- /dev/null +++ b/lib/federation/activity_pub/types/posts.ex @@ -0,0 +1,93 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Posts do + @moduledoc false + alias Mobilizon.{Actors, Posts} + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Posts.Post + require Logger + import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] + + @behaviour Entity + + @impl Entity + def create(args, additional) do + with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1), + {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- + Posts.create_post(args), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), + post_as_data <- + Convertible.model_to_as(%{post | attributed_to: group, author: creator}), + audience <- %{ + "to" => [group.members_url], + "cc" => [], + "actor" => creator_url, + "attributedTo" => [creator_url] + } do + create_data = make_create_data(post_as_data, Map.merge(audience, additional)) + + {:ok, post, create_data} + else + err -> + Logger.debug(inspect(err)) + err + end + end + + @impl Entity + def update(%Post{} = post, args, additional) do + with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1), + {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- + Posts.update_post(post, args), + {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), + post_as_data <- + Convertible.model_to_as(%{post | attributed_to: group, author: creator}), + audience <- %{ + "to" => [group.members_url], + "cc" => [], + "actor" => creator_url, + "attributedTo" => [creator_url] + } do + update_data = make_update_data(post_as_data, Map.merge(audience, additional)) + + {:ok, post, update_data} + else + err -> + Logger.debug(inspect(err)) + err + end + end + + @impl Entity + def delete( + %Post{ + url: url, + attributed_to: %Actor{url: group_url} + } = post, + %Actor{url: actor_url} = actor, + _local + ) do + activity_data = %{ + "actor" => actor_url, + "type" => "Delete", + "object" => Convertible.model_to_as(post), + "id" => url <> "/delete", + "to" => [group_url] + } + + with {:ok, _post} <- Posts.delete_post(post), + {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}") do + {:ok, activity_data, actor, post} + end + end + + def actor(%Post{author_id: author_id}), + do: Actors.get_actor(author_id) + + def group_actor(%Post{attributed_to_id: attributed_to_id}), + do: Actors.get_actor(attributed_to_id) +end diff --git a/lib/federation/activity_pub/types/reports.ex b/lib/federation/activity_pub/types/reports.ex new file mode 100644 index 000000000..ded8a1882 --- /dev/null +++ b/lib/federation/activity_pub/types/reports.ex @@ -0,0 +1,43 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Reports do + @moduledoc false + alias Mobilizon.{Actors, Discussions, Reports} + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Reports.Report + alias Mobilizon.Service.Formatter.HTML + require Logger + + def flag(args, local \\ false, _additional \\ %{}) do + with {:build_args, args} <- {:build_args, prepare_args_for_report(args)}, + {:create_report, {:ok, %Report{} = report}} <- + {:create_report, Reports.create_report(args)}, + report_as_data <- Convertible.model_to_as(report), + cc <- if(local, do: [report.reported.url], else: []), + report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do + {report, report_as_data} + end + end + + defp prepare_args_for_report(args) do + with {:reporter, %Actor{} = reporter_actor} <- + {:reporter, Actors.get_actor!(args.reporter_id)}, + {:reported, %Actor{} = reported_actor} <- + {:reported, Actors.get_actor!(args.reported_id)}, + content <- HTML.strip_tags(args.content), + event <- Discussions.get_comment(Map.get(args, :event_id)), + {:get_report_comments, comments} <- + {:get_report_comments, + Discussions.list_comments_by_actor_and_ids( + reported_actor.id, + Map.get(args, :comments_ids, []) + )} do + Map.merge(args, %{ + reporter: reporter_actor, + reported: reported_actor, + content: content, + event: event, + comments: comments + }) + end + end +end diff --git a/lib/federation/activity_pub/types/resources.ex b/lib/federation/activity_pub/types/resources.ex new file mode 100644 index 000000000..06a213096 --- /dev/null +++ b/lib/federation/activity_pub/types/resources.ex @@ -0,0 +1,157 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Resources do + @moduledoc false + alias Mobilizon.{Actors, Resources} + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Resources.Resource + alias Mobilizon.Service.RichMedia.Parser + require Logger + + import Mobilizon.Federation.ActivityPub.Utils, + only: [make_create_data: 2, make_update_data: 2, make_add_data: 3, make_move_data: 4] + + @behaviour Entity + + @impl Entity + def create(%{type: type} = args, additional) do + args = + case type do + :folder -> + args + + _ -> + case Parser.parse(Map.get(args, :resource_url)) do + {:ok, metadata} -> + Map.put(args, :metadata, metadata) + + _ -> + args + end + end + + with {:ok, + %Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <- + Resources.create_resource(args), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), + resource_as_data <- + Convertible.model_to_as(%{resource | actor: group, creator: creator}), + audience <- %{ + "to" => [group.members_url], + "cc" => [], + "actor" => creator_url, + "attributedTo" => [creator_url] + } do + create_data = + case parent_id do + nil -> + make_create_data(resource_as_data, Map.merge(audience, additional)) + + parent_id -> + # In case the resource has a parent we don't `Create` the resource but `Add` it to an existing resource + parent = Resources.get_resource(parent_id) + make_add_data(resource_as_data, parent, Map.merge(audience, additional)) + end + + {:ok, resource, create_data} + else + err -> + Logger.debug(inspect(err)) + err + end + end + + @impl Entity + def update(%Resource{} = old_resource, %{parent_id: _parent_id} = args, additional) do + move(old_resource, args, additional) + end + + # Simple rename + def update(%Resource{} = old_resource, %{title: title} = _args, additional) do + with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <- + Resources.update_resource(old_resource, %{title: title}), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + %Actor{url: creator_url} <- Actors.get_actor(creator_id), + resource_as_data <- + Convertible.model_to_as(%{resource | actor: group}), + audience <- %{ + "to" => [group.members_url], + "cc" => [], + "actor" => creator_url, + "attributedTo" => [creator_url] + }, + update_data <- + make_update_data(resource_as_data, Map.merge(audience, additional)) do + {:ok, resource, update_data} + else + err -> + Logger.debug(inspect(err)) + err + end + end + + def move( + %Resource{parent_id: old_parent_id} = old_resource, + %{parent_id: _new_parent_id} = args, + additional + ) do + with {:ok, + %Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} = + resource} <- + Resources.update_resource(old_resource, args), + old_parent <- Resources.get_resource(old_parent_id), + new_parent <- Resources.get_resource(new_parent_id), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + %Actor{url: creator_url} <- Actors.get_actor(creator_id), + resource_as_data <- + Convertible.model_to_as(%{resource | actor: group}), + audience <- %{ + "to" => [group.members_url], + "cc" => [], + "actor" => creator_url, + "attributedTo" => [creator_url] + }, + move_data <- + make_move_data( + resource_as_data, + old_parent, + new_parent, + Map.merge(audience, additional) + ) do + {:ok, resource, move_data} + else + err -> + Logger.debug(inspect(err)) + err + end + end + + @impl Entity + def delete( + %Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource, + %Actor{url: actor_url} = actor, + _local + ) do + Logger.debug("Building Delete Resource activity") + + activity_data = %{ + "actor" => actor_url, + "attributedTo" => [group_url], + "type" => "Delete", + "object" => Convertible.model_to_as(resource), + "id" => url <> "/delete", + "to" => [members_url] + } + + with {:ok, _resource} <- Resources.delete_resource(resource), + {:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do + {:ok, activity_data, actor, resource} + end + end + + def actor(%Resource{creator_id: creator_id}), + do: Actors.get_actor(creator_id) + + def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id) +end diff --git a/lib/federation/activity_pub/types/todo_lists.ex b/lib/federation/activity_pub/types/todo_lists.ex new file mode 100644 index 000000000..dedf9e489 --- /dev/null +++ b/lib/federation/activity_pub/types/todo_lists.ex @@ -0,0 +1,69 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do + @moduledoc false + alias Mobilizon.{Actors, Todos} + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Todos.TodoList + import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] + require Logger + + @behaviour Entity + + @impl Entity + @spec create(map(), map()) :: {:ok, map()} + def create(args, additional) do + with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}), + audience <- %{"to" => [group.members_url], "cc" => []}, + create_data <- + make_create_data(todo_list_as_data, Map.merge(audience, additional)) do + {:ok, todo_list, create_data} + end + end + + @impl Entity + @spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), Activity.t()} | any + def update(%TodoList{} = old_todo_list, args, additional) do + with {:ok, %TodoList{actor_id: group_id} = todo_list} <- + Todos.update_todo_list(old_todo_list, args), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + todo_list_as_data <- + Convertible.model_to_as(%{todo_list | actor: group}), + audience <- %{"to" => [group.members_url], "cc" => []}, + update_data <- + make_update_data(todo_list_as_data, Map.merge(audience, additional)) do + {:ok, todo_list, update_data} + end + end + + @impl Entity + @spec delete(TodoList.t(), Actor.t(), boolean()) :: + {:ok, ActivityStream.t(), Actor.t(), TodoList.t()} + def delete( + %TodoList{url: url, actor: %Actor{url: group_url}} = todo_list, + %Actor{url: actor_url} = actor, + _local + ) do + Logger.debug("Building Delete TodoList activity") + + activity_data = %{ + "actor" => actor_url, + "type" => "Delete", + "object" => Convertible.model_to_as(url), + "id" => url <> "/delete", + "to" => [group_url] + } + + with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list), + {:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do + {:ok, activity_data, actor, todo_list} + end + end + + def actor(%TodoList{}), do: nil + + def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id) +end diff --git a/lib/federation/activity_pub/types/todos.ex b/lib/federation/activity_pub/types/todos.ex new file mode 100644 index 000000000..dcf635f5c --- /dev/null +++ b/lib/federation/activity_pub/types/todos.ex @@ -0,0 +1,80 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Todos do + @moduledoc false + alias Mobilizon.{Actors, Todos} + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub.Types.Entity + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Todos.{Todo, TodoList} + import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] + require Logger + + @behaviour Entity + + @impl Entity + @spec create(map(), map()) :: {:ok, map()} + def create(args, additional) do + with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <- + Todos.create_todo(args), + %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id), + %Actor{} = creator <- Actors.get_actor(creator_id), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator}, + todo_as_data <- + Convertible.model_to_as(todo), + audience <- %{"to" => [group.members_url], "cc" => []}, + create_data <- + make_create_data(todo_as_data, Map.merge(audience, additional)) do + {:ok, todo, create_data} + end + end + + @impl Entity + @spec update(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any + def update(%Todo{} = old_todo, args, additional) do + with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args), + %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id), + {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), + todo_as_data <- + Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}), + audience <- %{"to" => [group.members_url], "cc" => []}, + update_data <- + make_update_data(todo_as_data, Map.merge(audience, additional)) do + {:ok, todo, update_data} + end + end + + @impl Entity + @spec delete(Todo.t(), Actor.t(), boolean()) :: {:ok, ActivityStream.t(), Actor.t(), Todo.t()} + def delete( + %Todo{url: url, creator: %Actor{url: group_url}} = todo, + %Actor{url: actor_url} = actor, + _local + ) do + Logger.debug("Building Delete Todo activity") + + activity_data = %{ + "actor" => actor_url, + "type" => "Delete", + "object" => Convertible.model_to_as(url), + "id" => url <> "/delete", + "to" => [group_url] + } + + with {:ok, _todo} <- Todos.delete_todo(todo), + {:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do + {:ok, activity_data, actor, todo} + end + end + + def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id) + + def group_actor(%Todo{todo_list_id: todo_list_id}) do + case Todos.get_todo_list(todo_list_id) do + %TodoList{actor_id: group_id} -> + Actors.get_actor(group_id) + + _ -> + nil + end + end +end diff --git a/lib/federation/activity_pub/types/tombstones.ex b/lib/federation/activity_pub/types/tombstones.ex new file mode 100644 index 000000000..b3f36cb6e --- /dev/null +++ b/lib/federation/activity_pub/types/tombstones.ex @@ -0,0 +1,14 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do + @moduledoc false + alias Mobilizon.{Actors, Tombstone} + alias Mobilizon.Actors.Actor + + def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id) + + def actor(%Tombstone{actor_id: actor_id}) when not is_nil(actor_id), + do: Actors.get_actor(actor_id) + + def actor(_), do: nil + + def group_actor(_), do: nil +end diff --git a/lib/federation/activity_pub/utils.ex b/lib/federation/activity_pub/utils.ex index 7a8d1d860..d59422e54 100644 --- a/lib/federation/activity_pub/utils.ex +++ b/lib/federation/activity_pub/utils.ex @@ -8,13 +8,16 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do Various ActivityPub related utils. """ + alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Media.Picture alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay} + alias Mobilizon.Federation.ActivityPub.Types.Ownable alias Mobilizon.Federation.ActivityStream.Converter alias Mobilizon.Federation.HTTPSignatures + alias Mobilizon.Web.Endpoint require Logger @@ -114,6 +117,53 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do def maybe_federate(_), do: :ok + @doc """ + Applies to activities sent by group members from outside this instance to a group of this instance, + we then need to relay (`Announce`) the object to other members on other instances. + """ + def maybe_relay_if_group_activity(activity, attributed_to \\ nil) + + def maybe_relay_if_group_activity( + %Activity{local: false, data: %{"object" => object}}, + _attributed_to + ) + when is_map(object) do + do_maybe_relay_if_group_activity(object, object["attributedTo"]) + end + + # When doing a delete the object is just an AP ID string, so we pass the attributed_to actor as well + def maybe_relay_if_group_activity( + %Activity{local: false, data: %{"object" => object}}, + %Actor{url: attributed_to_url} + ) + when is_binary(object) do + do_maybe_relay_if_group_activity(object, attributed_to_url) + end + + def maybe_relay_if_group_activity(_, _), do: :ok + + defp do_maybe_relay_if_group_activity(object, attributed_to) when not is_nil(attributed_to) do + id = "#{Endpoint.url()}/announces/#{Ecto.UUID.generate()}" + + case Actors.get_local_group_by_url(attributed_to) do + %Actor{} = group -> + case ActivityPub.announce(group, object, id, true, false) do + {:ok, _activity, _object} -> + Logger.info("Forwarded activity to external members of the group") + :ok + + _ -> + Logger.info("Failed to forward activity to external members of the group") + :error + end + + _ -> + :ok + end + end + + defp do_maybe_relay_if_group_activity(_, _), do: :ok + @spec remote_actors(list(String.t())) :: list(Actor.t()) def remote_actors(recipients) do recipients @@ -135,7 +185,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do Adds an id and a published data if they aren't there, also adds it to an included object """ - def lazy_put_activity_defaults(map) do + def lazy_put_activity_defaults(%{"object" => _object} = map) do if is_map(map["object"]) do object = lazy_put_object_defaults(map["object"]) %{map | "object" => object} @@ -147,7 +197,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do @doc """ Adds an id and published date if they aren't there. """ - def lazy_put_object_defaults(map) do + def lazy_put_object_defaults(map) when is_map(map) do Map.put_new_lazy(map, "published", &make_date/0) end @@ -175,25 +225,49 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do @doc """ Checks that an incoming AP object's actor matches the domain it came from. + + Takes the actor or attributedTo attributes (considers only the first elem if they're an array) """ + def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params) + when not is_nil(actor) and actor != "" do + params = Map.delete(params, "attributedTo") + origin_check?(id, params) + end + def origin_check?(id, %{"attributedTo" => actor} = params) do params = params |> Map.put("actor", actor) |> Map.delete("attributedTo") origin_check?(id, params) end - def origin_check?(id, %{"actor" => actor} = params) when not is_nil(actor) do - id_uri = URI.parse(id) - actor_uri = URI.parse(get_actor(params)) - - compare_uris?(actor_uri, id_uri) + def origin_check?(id, %{"actor" => actor} = params) + when not is_nil(actor) and is_list(actor) and length(actor) > 0 do + origin_check?(id, Map.put(params, "actor", hd(actor))) end - def origin_check?(_id, %{"actor" => nil}), do: false + def origin_check?(id, %{"actor" => actor} = params) + when not is_nil(actor) do + actor = get_actor(params) + Logger.debug("Performing origin check on #{id} and #{actor} URIs") + compare_origins?(id, actor) + end - def origin_check?(_id, _data), do: false + def origin_check?(_id, %{"type" => type} = _params) when type in ["Actor", "Group"], do: true + + def origin_check?(_id, %{"actor" => nil} = _args), do: false + + def origin_check?(_id, _args), do: false + + @spec compare_origins?(String.t(), String.t()) :: boolean() + def compare_origins?(url_1, url_2) when is_binary(url_1) and is_binary(url_2) do + uri_1 = URI.parse(url_1) + uri_2 = URI.parse(url_2) + + compare_uris?(uri_1, uri_2) + end defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), do: id_uri.host == other_uri.host + @spec origin_check_from_id?(String.t(), String.t()) :: boolean() def origin_check_from_id?(id, other_id) when is_binary(other_id) do id_uri = URI.parse(id) other_uri = URI.parse(other_id) @@ -201,9 +275,20 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do compare_uris?(id_uri, other_uri) end + @spec origin_check_from_id?(String.t(), map()) :: boolean() def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id), do: origin_check_from_id?(id, other_id) + def activity_actor_is_group_member?(%Actor{id: actor_id}, object) do + case Ownable.group_actor(object) do + %Actor{type: :Group, id: group_id} -> + Actors.is_member?(actor_id, group_id) + + _ -> + false + end + end + @doc """ Save picture data from %Plug.Upload{} and return AS Link data. """ @@ -274,7 +359,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do activity_id, public ) - when type in ["Note", "Event", "ResourceCollection", "Document"] do + when type in ["Note", "Event", "ResourceCollection", "Document", "Todo"] do do_make_announce_data( actor, object_actor_url, @@ -367,6 +452,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do "type" => "Create", "to" => object["to"], "cc" => object["cc"], + "attributedTo" => object["attributedTo"] || object["actor"], "actor" => object["actor"], "object" => object, "published" => make_date(), @@ -494,7 +580,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do @doc """ Sign a request with the instance Relay actor. """ - @spec sign_fetch_relay(List.t(), String.t(), String.t()) :: List.t() + @spec sign_fetch_relay(Enum.t(), String.t(), String.t()) :: Enum.t() def sign_fetch_relay(headers, id, date) do with %Actor{} = actor <- Relay.get_actor() do sign_fetch(headers, actor, id, date) @@ -504,7 +590,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do @doc """ Sign a request with an actor. """ - @spec sign_fetch(List.t(), Actor.t(), String.t(), String.t()) :: List.t() + @spec sign_fetch(Enum.t(), Actor.t(), String.t(), String.t()) :: Enum.t() def sign_fetch(headers, actor, id, date) do if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do headers ++ make_signature(actor, id, date) @@ -516,7 +602,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do @doc """ Add the Date header to the request if we sign object fetches """ - @spec maybe_date_fetch(List.t(), String.t()) :: List.t() + @spec maybe_date_fetch(Enum.t(), String.t()) :: Enum.t() def maybe_date_fetch(headers, date) do if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do headers ++ [{:Date, date}] @@ -524,4 +610,15 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do headers end end + + def check_for_actor_key_rotation(%Actor{} = actor) do + if Actors.should_rotate_actor_key(actor) do + Actors.schedule_key_rotation( + actor, + Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay] + ) + end + + :ok + end end diff --git a/lib/federation/activity_pub/visibility.ex b/lib/federation/activity_pub/visibility.ex index e63f92890..b8599e880 100644 --- a/lib/federation/activity_pub/visibility.ex +++ b/lib/federation/activity_pub/visibility.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.Federation.ActivityPub.Visibility do Utility functions related to content visibility """ - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Federation.ActivityPub.Activity diff --git a/lib/federation/activity_stream.ex b/lib/federation/activity_stream.ex new file mode 100644 index 000000000..f6d7a8132 --- /dev/null +++ b/lib/federation/activity_stream.ex @@ -0,0 +1,7 @@ +defmodule Mobilizon.Federation.ActivityStream do + @moduledoc """ + The ActivityStream Type + """ + + @type t :: map() +end diff --git a/lib/federation/activity_stream/converter/actor.ex b/lib/federation/activity_stream/converter/actor.ex index 39fd5dd3c..54ad8bfa2 100644 --- a/lib/federation/activity_stream/converter/actor.ex +++ b/lib/federation/activity_stream/converter/actor.ex @@ -49,7 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do banner: banner, name: data["name"], preferred_username: data["preferredUsername"], - summary: data["summary"], + summary: data["summary"] || "", keys: data["publicKey"]["publicKeyPem"], inbox_url: data["inbox"], outbox_url: data["outbox"], @@ -57,6 +57,10 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do followers_url: data["followers"], members_url: data["members"], resources_url: data["resources"], + todos_url: data["todos"], + events_url: data["events"], + posts_url: data["posts"], + discussions_url: data["discussions"], shared_inbox_url: data["endpoints"]["sharedInbox"], domain: URI.parse(data["id"]).host, manually_approves_followers: data["manuallyApprovesFollowers"], @@ -77,12 +81,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do "type" => actor.type, "preferredUsername" => actor.preferred_username, "name" => actor.name, - "summary" => actor.summary, + "summary" => actor.summary || "", "following" => actor.following_url, "followers" => actor.followers_url, "members" => actor.members_url, "resources" => actor.resources_url, "todos" => actor.todos_url, + "posts" => actor.posts_url, + "events" => actor.events_url, + "discussions" => actor.discussions_url, "inbox" => actor.inbox_url, "outbox" => actor.outbox_url, "url" => actor.url, diff --git a/lib/federation/activity_stream/converter/comment.ex b/lib/federation/activity_stream/converter/comment.ex index a3310614c..e5075c64c 100644 --- a/lib/federation/activity_stream/converter/comment.ex +++ b/lib/federation/activity_stream/converter/comment.ex @@ -7,22 +7,30 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do """ alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment, as: CommentModel + alias Mobilizon.Discussions + alias Mobilizon.Discussions.Comment, as: CommentModel + alias Mobilizon.Discussions.Discussion alias Mobilizon.Events.Event - alias Mobilizon.Tombstone, as: TombstoneModel - alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Visibility alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} - alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils + alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter + alias Mobilizon.Tombstone, as: TombstoneModel + + import Mobilizon.Federation.ActivityStream.Converter.Utils, + only: [ + fetch_tags: 1, + fetch_mentions: 1, + build_tags: 1, + build_mentions: 1, + maybe_fetch_actor_and_attributed_to_id: 1 + ] require Logger @behaviour Converter defimpl Convertible, for: CommentModel do - alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter - defdelegate model_to_as(comment), to: CommentConverter end @@ -35,61 +43,35 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do Logger.debug("We're converting raw ActivityStream data to a comment entity") Logger.debug(inspect(object)) - with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"), - {:ok, %Actor{id: actor_id, domain: domain, suspended: false}} <- - ActivityPub.get_or_fetch_actor_by_url(author_url), - {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))}, + with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <- + maybe_fetch_actor_and_attributed_to_id(object), + {:tags, tags} <- {:tags, fetch_tags(Map.get(object, "tag", []))}, {:mentions, mentions} <- - {:mentions, ConverterUtils.fetch_mentions(Map.get(object, "tag", []))} do + {:mentions, fetch_mentions(Map.get(object, "tag", []))}, + discussion <- + Discussions.get_discussion_by_url(Map.get(object, "context")) do Logger.debug("Inserting full comment") Logger.debug(inspect(object)) data = %{ text: object["content"], url: object["id"], + # Will be used in conversations, ignored in basic comments + title: object["name"], + context: object["context"], actor_id: actor_id, + attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id), in_reply_to_comment_id: nil, event_id: nil, uuid: object["uuid"], + discussion_id: if(is_nil(discussion), do: nil, else: discussion.id), tags: tags, mentions: mentions, - local: is_nil(domain), + local: is_nil(actor_domain), visibility: if(Visibility.is_public?(object), do: :public, else: :private) } - # We fetch the parent object - Logger.debug("We're fetching the parent object") - - if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil && - object["inReplyTo"] != "" do - Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end) - - case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do - # Reply to an event (Event) - {:ok, %Event{id: id}} -> - Logger.debug("Parent object is an event") - data |> Map.put(:event_id, id) - - # Reply to a comment (Comment) - {:ok, %CommentModel{id: id} = comment} -> - Logger.debug("Parent object is another comment") - - data - |> Map.put(:in_reply_to_comment_id, id) - |> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id()) - |> Map.put(:event_id, comment.event_id) - - # Anything else is kind of a MP - {:error, parent} -> - Logger.warn("Parent object is something we don't handle") - Logger.debug(inspect(parent)) - data - end - else - Logger.debug("No parent object for this comment") - - data - end + maybe_fetch_parent_object(object, data) else {:ok, %Actor{suspended: true}} -> :error @@ -102,10 +84,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do @impl Converter @spec model_to_as(CommentModel.t()) :: map def model_to_as(%CommentModel{deleted_at: nil} = comment) do - to = - if comment.visibility == :public, - do: ["https://www.w3.org/ns/activitystreams#Public"], - else: [comment.actor.followers_url] + to = determine_to(comment) object = %{ "type" => "Note", @@ -114,13 +93,19 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do "content" => comment.text, "mediaType" => "text/html", "actor" => comment.actor.url, - "attributedTo" => comment.actor.url, + "attributedTo" => + if(is_nil(comment.attributed_to), do: nil, else: comment.attributed_to.url) || + comment.actor.url, "uuid" => comment.uuid, "id" => comment.url, - "tag" => - ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags) + "tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags) } + object = + if comment.discussion_id, + do: Map.put(object, "context", comment.discussion.url), + else: object + cond do comment.in_reply_to_comment -> Map.put(object, "inReplyTo", comment.in_reply_to_comment.url) @@ -133,15 +118,78 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do end end - @impl Converter - @spec model_to_as(CommentModel.t()) :: map @doc """ A "soft-deleted" comment is a tombstone """ + @impl Converter + @spec model_to_as(CommentModel.t()) :: map def model_to_as(%CommentModel{} = comment) do Convertible.model_to_as(%TombstoneModel{ uri: comment.url, inserted_at: comment.deleted_at }) end + + @spec determine_to(CommentModel.t()) :: [String.t()] + defp determine_to(%CommentModel{} = comment) do + cond do + not is_nil(comment.attributed_to) -> + [comment.attributed_to.url] + + comment.visibility == :public -> + ["https://www.w3.org/ns/activitystreams#Public"] + + true -> + [comment.actor.followers_url] + end + end + + defp maybe_fetch_parent_object(object, data) do + # We fetch the parent object + Logger.debug("We're fetching the parent object") + + if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil && + object["inReplyTo"] != "" do + Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end) + + case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do + # Reply to an event (Event) + {:ok, %Event{id: id}} -> + Logger.debug("Parent object is an event") + data |> Map.put(:event_id, id) + + # Reply to a comment (Comment) + {:ok, %CommentModel{id: id} = comment} -> + Logger.debug("Parent object is another comment") + + data + |> Map.put(:in_reply_to_comment_id, id) + |> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id()) + |> Map.put(:event_id, comment.event_id) + + # Reply to a discucssion (Discussion) + {:ok, + %Discussion{ + id: discussion_id, + last_comment: %CommentModel{id: last_comment_id, origin_comment_id: origin_comment_id} + } = _discussion} -> + Logger.debug("Parent object is a discussion") + + data + |> Map.put(:in_reply_to_comment_id, last_comment_id) + |> Map.put(:origin_comment_id, origin_comment_id) + |> Map.put(:discussion_id, discussion_id) + + # Anything else is kind of a MP + {:error, parent} -> + Logger.warn("Parent object is something we don't handle") + Logger.debug(inspect(parent)) + data + end + else + Logger.debug("No parent object for this comment") + + data + end + end end diff --git a/lib/federation/activity_stream/converter/converter.ex b/lib/federation/activity_stream/converter/converter.ex index 4398304f5..73b1ca69b 100644 --- a/lib/federation/activity_stream/converter/converter.ex +++ b/lib/federation/activity_stream/converter/converter.ex @@ -6,6 +6,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter do one, and back. """ - @callback as_to_model_data(map) :: map - @callback model_to_as(struct) :: map + @type model_data :: map() + + @callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data() + @callback model_to_as(model :: struct()) :: ActivityStream.t() end diff --git a/lib/federation/activity_stream/converter/discussion.ex b/lib/federation/activity_stream/converter/discussion.ex new file mode 100644 index 000000000..514e0dfa2 --- /dev/null +++ b/lib/federation/activity_stream/converter/discussion.ex @@ -0,0 +1,63 @@ +defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do + @moduledoc """ + Comment converter. + + This module allows to convert events from ActivityStream format to our own + internal one, and back. + """ + + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions.Discussion + alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} + alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter + alias Mobilizon.Storage.Repo + + require Logger + + @behaviour Converter + + defimpl Convertible, for: Discussion do + defdelegate model_to_as(comment), to: DiscussionConverter + end + + @doc """ + Make an AS comment object from an existing `discussion` structure. + """ + @impl Converter + @spec model_to_as(Discussion.t()) :: map + def model_to_as(%Discussion{} = discussion) do + discussion = Repo.preload(discussion, [:last_comment, :actor, :creator]) + + %{ + "type" => "Note", + "to" => [discussion.actor.followers_url], + "cc" => [], + "name" => discussion.title, + "content" => discussion.last_comment.text, + "mediaType" => "text/html", + "actor" => discussion.creator.url, + "attributedTo" => discussion.actor.url, + "id" => discussion.url, + "context" => discussion.url + } + end + + @impl Converter + @spec as_to_model_data(map) :: {:ok, map} | {:error, any()} + def as_to_model_data(%{"type" => "Note", "name" => name} = object) when not is_nil(name) do + with creator_url <- Map.get(object, "actor"), + {:ok, %Actor{id: creator_id, suspended: false}} <- + ActivityPub.get_or_fetch_actor_by_url(creator_url), + actor_url <- Map.get(object, "attributedTo"), + {:ok, %Actor{id: actor_id, suspended: false}} <- + ActivityPub.get_or_fetch_actor_by_url(actor_url) do + %{ + title: name, + actor_id: actor_id, + creator_id: creator_id, + url: object["id"] + } + end + end +end diff --git a/lib/federation/activity_stream/converter/event.ex b/lib/federation/activity_stream/converter/event.ex index 7319752a8..04dff326b 100644 --- a/lib/federation/activity_stream/converter/event.ex +++ b/lib/federation/activity_stream/converter/event.ex @@ -12,11 +12,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do alias Mobilizon.Events.Event, as: EventModel alias Mobilizon.Media.Picture - alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter alias Mobilizon.Federation.ActivityStream.Converter.Picture, as: PictureConverter - alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils + + import Mobilizon.Federation.ActivityStream.Converter.Utils, + only: [ + fetch_tags: 1, + fetch_mentions: 1, + build_tags: 1, + maybe_fetch_actor_and_attributed_to_id: 1 + ] require Logger @@ -34,16 +40,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do @impl Converter @spec as_to_model_data(map) :: {:ok, map()} | {:error, any()} def as_to_model_data(object) do - Logger.debug("event as_to_model_data") - Logger.debug(inspect(object)) - - with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"), - {:actor, {:ok, %Actor{id: actor_id, domain: actor_domain, suspended: false}}} <- - {:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)}, + with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <- + maybe_fetch_actor_and_attributed_to_id(object), {:address, address_id} <- {:address, get_address(object["location"])}, - {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])}, - {:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])}, + {:tags, tags} <- {:tags, fetch_tags(object["tag"])}, + {:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])}, {:visibility, visibility} <- {:visibility, get_visibility(object)}, {:options, options} <- {:options, get_options(object)} do attachments = @@ -67,6 +69,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do title: object["name"], description: object["content"], organizer_actor_id: actor_id, + attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id), picture_id: picture_id, begins_on: object["startTime"], ends_on: object["endTime"], @@ -108,7 +111,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do "type" => "Event", "to" => to, "cc" => [], - "attributedTo" => event.organizer_actor.url, + "attributedTo" => + if(is_nil(event.attributed_to), do: nil, else: event.attributed_to.url) || + event.organizer_actor.url, "name" => event.title, "actor" => event.organizer_actor.url, "uuid" => event.uuid, @@ -120,7 +125,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do "startTime" => event.begins_on |> date_to_string(), "joinMode" => to_string(event.join_options), "endTime" => event.ends_on |> date_to_string(), - "tag" => event.tags |> ConverterUtils.build_tags(), + "tag" => event.tags |> build_tags(), "maximumAttendeeCapacity" => event.options.maximum_attendee_capacity, "repliesModerationOption" => event.options.comment_moderation, "commentsEnabled" => event.options.comment_moderation == :allow_all, diff --git a/lib/federation/activity_stream/converter/flag.ex b/lib/federation/activity_stream/converter/flag.ex index f3abb7a74..975b78a84 100644 --- a/lib/federation/activity_stream/converter/flag.ex +++ b/lib/federation/activity_stream/converter/flag.ex @@ -9,7 +9,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do """ alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations + alias Mobilizon.Discussions alias Mobilizon.Events alias Mobilizon.Events.Event alias Mobilizon.Reports.Report @@ -92,7 +92,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do Enum.filter(objects, fn url -> !(url == reported.url || (!is_nil(event) && event.url == url)) end), - comments <- Enum.map(comments, &Conversations.get_comment_from_url/1) do + comments <- Enum.map(comments, &Discussions.get_comment_from_url/1) do %{ "reporter" => reporter, "uri" => object["id"], diff --git a/lib/federation/activity_stream/converter/picture.ex b/lib/federation/activity_stream/converter/picture.ex index fd0af3eae..e5c666a6c 100644 --- a/lib/federation/activity_stream/converter/picture.ex +++ b/lib/federation/activity_stream/converter/picture.ex @@ -39,7 +39,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Picture do actor_id ) when is_bitstring(picture_url) do - with {:ok, %HTTPoison.Response{body: body}} <- HTTPoison.get(picture_url, [], @http_options), + with {:ok, %{body: body}} <- Tesla.get(picture_url, opts: @http_options), {:ok, %{name: name, url: url, content_type: content_type, size: size}} <- Upload.store(%{body: body, name: name}), {:picture_exists, nil} <- {:picture_exists, Media.get_picture_by_url(url)} do diff --git a/lib/federation/activity_stream/converter/post.ex b/lib/federation/activity_stream/converter/post.ex new file mode 100644 index 000000000..41e715f93 --- /dev/null +++ b/lib/federation/activity_stream/converter/post.ex @@ -0,0 +1,70 @@ +defmodule Mobilizon.Federation.ActivityStream.Converter.Post do + @moduledoc """ + Post converter. + + This module allows to convert posts from ActivityStream format to our own + internal one, and back. + """ + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Utils + alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} + alias Mobilizon.Posts.Post + require Logger + + @behaviour Converter + + defimpl Convertible, for: Post do + alias Mobilizon.Federation.ActivityStream.Converter.Post, as: PostConverter + + defdelegate model_to_as(post), to: PostConverter + end + + @doc """ + Convert an post struct to an ActivityStream representation + """ + @impl Converter + @spec model_to_as(Post.t()) :: map + def model_to_as( + %Post{author: %Actor{url: actor_url}, attributed_to: %Actor{url: creator_url}} = post + ) do + %{ + "type" => "Article", + "actor" => actor_url, + "id" => post.url, + "name" => post.title, + "content" => post.body, + "attributedTo" => creator_url, + "published" => post.publish_at || post.inserted_at + } + end + + @doc """ + Converts an AP object data to our internal data structure. + """ + @impl Converter + @spec as_to_model_data(map) :: {:ok, map} | {:error, any()} + def as_to_model_data( + %{"type" => "Article", "actor" => creator, "attributedTo" => group} = object + ) do + with {:ok, %Actor{id: attributed_to_id}} <- get_actor(group), + {:ok, %Actor{id: author_id}} <- get_actor(creator) do + %{ + title: object["name"], + body: object["content"], + url: object["id"], + attributed_to_id: attributed_to_id, + author_id: author_id, + local: false, + publish_at: object["published"] + } + else + {:error, err} -> {:error, err} + err -> {:error, err} + end + end + + @spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()} + defp get_actor(nil), do: {:error, "nil property found for actor data"} + defp get_actor(actor), do: actor |> Utils.get_url() |> ActivityPub.get_or_fetch_actor_by_url() +end diff --git a/lib/federation/activity_stream/converter/todo_list.ex b/lib/federation/activity_stream/converter/todo_list.ex index ad075710c..66c1776e4 100644 --- a/lib/federation/activity_stream/converter/todo_list.ex +++ b/lib/federation/activity_stream/converter/todo_list.ex @@ -28,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do "type" => "TodoList", "actor" => group_url, "id" => todo_list.url, - "title" => todo_list.title + "name" => todo_list.title } end diff --git a/lib/federation/activity_stream/converter/tombstone.ex b/lib/federation/activity_stream/converter/tombstone.ex index d0712286f..642e26485 100644 --- a/lib/federation/activity_stream/converter/tombstone.ex +++ b/lib/federation/activity_stream/converter/tombstone.ex @@ -28,6 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Tombstone do %{ "type" => "Tombstone", "id" => tombstone.uri, + "actor" => tombstone.actor.url, "deleted" => tombstone.inserted_at } end diff --git a/lib/federation/activity_stream/converter/utils.ex b/lib/federation/activity_stream/converter/utils.ex index a47cc6f79..0c296cdfc 100644 --- a/lib/federation/activity_stream/converter/utils.ex +++ b/lib/federation/activity_stream/converter/utils.ex @@ -23,6 +23,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do tags |> Enum.flat_map(&fetch_tag/1) |> Enum.uniq() |> Enum.map(&existing_tag_or_data/1) end + def fetch_tags(_), do: [] + @spec fetch_mentions([map()]) :: [map()] def fetch_mentions(mentions) when is_list(mentions) do Logger.debug("fetching mentions") @@ -30,6 +32,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end) end + def fetch_mentions(_), do: [] + def fetch_address(%{id: id}) do with {id, ""} <- Integer.parse(id), do: %{id: id} end @@ -38,7 +42,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do address end - @spec build_tags([Tag.t()]) :: [Map.t()] + @spec build_tags([Tag.t()]) :: [map()] def build_tags(tags) do Enum.map(tags, fn %Tag{} = tag -> %{ @@ -111,4 +115,51 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do defp create_mention({_, mention}, acc) when is_map(mention) do create_mention(mention, acc) end + + @spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil} + def maybe_fetch_actor_and_attributed_to_id(%{ + "actor" => actor_url, + "attributedTo" => attributed_to_url + }) + when is_nil(attributed_to_url) do + {fetch_actor(actor_url), nil} + end + + @spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil} + def maybe_fetch_actor_and_attributed_to_id(%{ + "actor" => actor_url, + "attributedTo" => attributed_to_url + }) + when is_nil(actor_url) do + {fetch_actor(attributed_to_url), nil} + end + + # Only when both actor and attributedTo fields are both filled is when we can return both + def maybe_fetch_actor_and_attributed_to_id(%{ + "actor" => actor_url, + "attributedTo" => attributed_to_url + }) + when actor_url != attributed_to_url do + with actor <- fetch_actor(actor_url), + attributed_to <- fetch_actor(attributed_to_url) do + {actor, attributed_to} + end + end + + # If we only have attributedTo and no actor, take attributedTo as the actor + def maybe_fetch_actor_and_attributed_to_id(%{ + "attributedTo" => attributed_to_url + }) do + {fetch_actor(attributed_to_url), nil} + end + + def maybe_fetch_actor_and_attributed_to_id(_), do: {nil, nil} + + @spec fetch_actor(String.t()) :: Actor.t() + defp fetch_actor(actor_url) do + with {:ok, %Actor{suspended: false} = actor} <- + ActivityPub.get_or_fetch_actor_by_url(actor_url) do + actor + end + end end diff --git a/lib/federation/activity_stream/convertible.ex b/lib/federation/activity_stream/convertible.ex index cf6e2dfc1..776161b1e 100644 --- a/lib/federation/activity_stream/convertible.ex +++ b/lib/federation/activity_stream/convertible.ex @@ -3,8 +3,9 @@ defprotocol Mobilizon.Federation.ActivityStream.Convertible do Convertible protocol. """ - @type activity_streams :: map + @type t :: struct() + @type activity_streams :: map() - @spec model_to_as(t) :: activity_streams + @spec model_to_as(t()) :: activity_streams() def model_to_as(convertible) end diff --git a/lib/federation/web_finger/web_finger.ex b/lib/federation/web_finger/web_finger.ex index 1b939aeba..84b496a40 100644 --- a/lib/federation/web_finger/web_finger.ex +++ b/lib/federation/web_finger/web_finger.ex @@ -118,13 +118,15 @@ defmodule Mobilizon.Federation.WebFinger do Logger.debug(inspect(address)) with false <- is_nil(domain), - {:ok, %HTTPoison.Response{} = response} <- - HTTPoison.get( + {:ok, %{} = response} <- + Tesla.get( address, - [Accept: "application/json, application/activity+json, application/jrd+json"], - @http_options + headers: [ + {"accept", "application/json, application/activity+json, application/jrd+json"} + ], + opts: @http_options ), - %{status_code: status_code, body: body} when status_code in 200..299 <- response, + %{status: status, body: body} when status in 200..299 <- response, {:ok, doc} <- Jason.decode(body) do webfinger_from_json(doc) else diff --git a/lib/graphql/api/comments.ex b/lib/graphql/api/comments.ex index 16c9b5f38..1d7033dbe 100644 --- a/lib/graphql/api/comments.ex +++ b/lib/graphql/api/comments.ex @@ -3,8 +3,8 @@ defmodule Mobilizon.GraphQL.API.Comments do API for Comments. """ - alias Mobilizon.Conversations.Comment - + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions.Comment alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Activity @@ -19,7 +19,7 @@ defmodule Mobilizon.GraphQL.API.Comments do end def update_comment(%Comment{} = comment, args) do - ActivityPub.update(:comment, comment, args, true) + ActivityPub.update(comment, args, true) end @doc """ @@ -27,8 +27,8 @@ defmodule Mobilizon.GraphQL.API.Comments do Deletes a comment from an actor """ - @spec delete_comment(Comment.t()) :: {:ok, Activity.t(), Comment.t()} | any - def delete_comment(%Comment{} = comment) do - ActivityPub.delete(comment, true) + @spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any + def delete_comment(%Comment{} = comment, %Actor{} = actor) do + ActivityPub.delete(comment, actor, true) end end diff --git a/lib/graphql/api/events.ex b/lib/graphql/api/events.ex index fdbb92ae8..a7ebbd75c 100644 --- a/lib/graphql/api/events.ex +++ b/lib/graphql/api/events.ex @@ -34,7 +34,7 @@ defmodule Mobilizon.GraphQL.API.Events do Map.update(args, :picture, nil, fn picture -> process_picture(picture, organizer_actor) end) do - ActivityPub.update(:event, event, args, Map.get(args, :draft, false) == false) + ActivityPub.update(event, args, Map.get(args, :draft, false) == false) end end @@ -43,8 +43,8 @@ defmodule Mobilizon.GraphQL.API.Events do If the event is deleted by """ - def delete_event(%Event{} = event, federate \\ true) do - ActivityPub.delete(event, federate) + def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do + ActivityPub.delete(event, actor, federate) end defp process_picture(nil, _), do: nil diff --git a/lib/graphql/api/groups.ex b/lib/graphql/api/groups.ex index 315853ede..e813d0041 100644 --- a/lib/graphql/api/groups.ex +++ b/lib/graphql/api/groups.ex @@ -19,8 +19,25 @@ defmodule Mobilizon.GraphQL.API.Groups do args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(), {:existing_group, nil} <- {:existing_group, Actors.get_local_group_by_title(preferred_username)}, + args <- args |> Map.put(:type, :Group), {:ok, %Activity{} = activity, %Actor{} = group} <- - ActivityPub.create(:group, args, true, %{"actor" => args.creator_actor.url}) do + ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do + {:ok, activity, group} + else + {:existing_group, _} -> + {:error, "A group with this name already exists"} + + {:is_owned, nil} -> + {:error, "Actor id is not owned by authenticated user"} + end + end + + @spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any + def update_group(%{id: id} = args) do + with {:existing_group, {:ok, %Actor{type: :Group} = group}} <- + {:existing_group, Actors.get_group_by_actor_id(id)}, + {:ok, %Activity{} = activity, %Actor{} = group} <- + ActivityPub.update(group, args, true, %{"actor" => args.updater_actor.url}) do {:ok, activity, group} else {:existing_group, _} -> diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex index 5ccb9eea5..bef94265a 100644 --- a/lib/graphql/resolvers/admin.ex +++ b/lib/graphql/resolvers/admin.ex @@ -9,7 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do alias Mobilizon.Actors.Actor alias Mobilizon.Admin.{ActionLog, Setting} alias Mobilizon.Config - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Relay @@ -297,7 +297,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do with {:changes, true} <- {:changes, args != %{}}, %Actor{} = instance_actor <- Relay.get_actor(), - {:ok, _activity, _actor} <- ActivityPub.update(:actor, instance_actor, args, true) do + {:ok, _activity, _actor} <- ActivityPub.update(instance_actor, args, true) do :ok else {:changes, false} -> diff --git a/lib/graphql/resolvers/comment.ex b/lib/graphql/resolvers/comment.ex index fa52dca7b..6ccb7a455 100644 --- a/lib/graphql/resolvers/comment.ex +++ b/lib/graphql/resolvers/comment.ex @@ -3,9 +3,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do Handles the comment-related GraphQL calls. """ - alias Mobilizon.{Actors, Admin, Conversations} + alias Mobilizon.{Actors, Admin, Discussions} alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment, as: CommentModel + alias Mobilizon.Discussions.Comment, as: CommentModel alias Mobilizon.Users alias Mobilizon.Users.User @@ -14,7 +14,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do require Logger def get_thread(_parent, %{id: thread_id}, _context) do - {:ok, Conversations.get_thread_replies(thread_id)} + {:ok, Discussions.get_thread_replies(thread_id)} end def create_comment( @@ -51,7 +51,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do ) do with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, %CommentModel{actor_id: comment_actor_id} = comment <- - Mobilizon.Conversations.get_comment(comment_id), + Mobilizon.Discussions.get_comment(comment_id), true <- actor_id === comment_actor_id, {:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do {:ok, comment} @@ -72,15 +72,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do } ) do with {actor_id, ""} <- Integer.parse(actor_id), - {:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id), + {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), %CommentModel{deleted_at: nil} = comment <- - Conversations.get_comment_with_preload(comment_id) do + Discussions.get_comment_with_preload(comment_id) do cond do {:comment_can_be_managed, true} == CommentModel.can_be_managed_by(comment, actor_id) -> - do_delete_comment(comment) + do_delete_comment(comment, actor) role in [:moderator, :administrator] -> - with {:ok, res} <- do_delete_comment(comment), + with {:ok, res} <- do_delete_comment(comment, actor), %Actor{} = actor <- Actors.get_actor(actor_id) do Admin.log_action(actor, "delete", comment) @@ -103,9 +103,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do {:error, "You are not allowed to delete a comment if not connected"} end - defp do_delete_comment(%CommentModel{} = comment) do + defp do_delete_comment(%CommentModel{} = comment, %Actor{} = actor) do with {:ok, _, %CommentModel{} = comment} <- - Comments.delete_comment(comment) do + Comments.delete_comment(comment, actor) do {:ok, comment} end end diff --git a/lib/graphql/resolvers/conversation.ex b/lib/graphql/resolvers/conversation.ex deleted file mode 100644 index 0baab03b6..000000000 --- a/lib/graphql/resolvers/conversation.ex +++ /dev/null @@ -1,110 +0,0 @@ -defmodule Mobilizon.GraphQL.Resolvers.Conversation do - @moduledoc """ - Handles the group-related GraphQL calls. - """ - - alias Mobilizon.{Actors, Conversations, Users} - alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Conversation, as: ConversationModel - alias Mobilizon.Storage.Page - alias Mobilizon.Users.User - - def find_conversations_for_actor( - %Actor{id: group_id}, - _args, - %{ - context: %{ - current_user: %User{} = user - } - } - ) do - with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, - {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do - {:ok, Conversations.find_conversations_for_actor(group_id)} - else - {:member, false} -> - {:ok, %Page{total: 0, elements: []}} - end - end - - def find_conversations_for_actor(%Actor{}, _args, _resolution) do - {:ok, %Page{total: 0, elements: []}} - end - - def get_conversation(_parent, %{id: id}, _resolution) do - {:ok, Conversations.get_conversation(id)} - end - - def get_comments_for_conversation( - %ConversationModel{id: conversation_id}, - %{page: page, limit: limit}, - _resolution - ) do - {:ok, Conversations.get_comments_for_conversation(conversation_id, page, limit)} - end - - def create_conversation( - _parent, - %{title: title, text: text, actor_id: actor_id, creator_id: creator_id}, - _resolution - ) do - with {:ok, %ConversationModel{} = conversation} <- - Conversations.create_conversation(%{ - title: title, - text: text, - actor_id: actor_id, - creator_id: creator_id - }) do - {:ok, conversation} - end - end - - def reply_to_conversation( - _parent, - %{text: text, conversation_id: conversation_id}, - %{ - context: %{ - current_user: %User{} = user - } - } - ) do - with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, - {:no_conversation, %ConversationModel{} = conversation} <- - {:no_conversation, Conversations.get_conversation(conversation_id)}, - {:ok, %ConversationModel{} = conversation} <- - Conversations.reply_to_conversation( - conversation, - %{ - text: text, - actor_id: actor_id - } - ) do - {:ok, conversation} - end - end - - @spec update_conversation(map(), map(), map()) :: {:ok, ConversationModel.t()} - def update_conversation( - _parent, - %{title: title, conversation_id: conversation_id}, - %{ - context: %{ - current_user: %User{} = user - } - } - ) do - with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, - {:no_conversation, %ConversationModel{creator_id: creator_id} = conversation} <- - {:no_conversation, Conversations.get_conversation(conversation_id)}, - {:check_access, true} <- {:check_access, actor_id == creator_id}, - {:ok, %ConversationModel{} = conversation} <- - Conversations.update_conversation( - conversation, - %{ - title: title - } - ) do - {:ok, conversation} - end - end -end diff --git a/lib/graphql/resolvers/discussion.ex b/lib/graphql/resolvers/discussion.ex new file mode 100644 index 000000000..cc0010dfb --- /dev/null +++ b/lib/graphql/resolvers/discussion.ex @@ -0,0 +1,179 @@ +defmodule Mobilizon.GraphQL.Resolvers.Discussion do + @moduledoc """ + Handles the group-related GraphQL calls. + """ + + alias Mobilizon.{Actors, Discussions, Users} + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions.{Comment, Discussion} + alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Storage.Page + alias Mobilizon.Users.User + + def find_discussions_for_actor( + %Actor{id: group_id}, + _args, + %{ + context: %{ + current_user: %User{} = user + } + } + ) do + with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, + {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do + {:ok, Discussions.find_discussions_for_actor(group_id)} + else + {:member, false} -> + {:ok, %Page{total: 0, elements: []}} + end + end + + def find_discussions_for_actor(%Actor{}, _args, _resolution) do + {:ok, %Page{total: 0, elements: []}} + end + + def get_discussion(_parent, %{id: id}, %{ + context: %{ + current_user: %User{} = user + } + }) do + with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, + %Discussion{actor_id: actor_id} = discussion <- + Discussions.get_discussion(id), + {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do + {:ok, discussion} + end + end + + def get_discussion(_parent, %{slug: slug}, %{ + context: %{ + current_user: %User{} = user + } + }) do + with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, + %Discussion{actor_id: actor_id} = discussion <- + Discussions.get_discussion_by_slug(slug), + {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do + {:ok, discussion} + else + nil -> {:error, "No such discussion"} + end + end + + def get_discussion(_parent, _args, _resolution), + do: {:error, "You need to be logged-in to access discussions"} + + def get_comments_for_discussion( + %Discussion{id: discussion_id}, + %{page: page, limit: limit}, + _resolution + ) do + {:ok, Discussions.get_comments_for_discussion(discussion_id, page, limit)} + end + + def create_discussion( + _parent, + %{title: title, text: text, actor_id: actor_id}, + %{ + context: %{ + current_user: %User{} = user + } + } + ) do + with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, + {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)}, + {:ok, _activity, %Discussion{} = discussion} <- + ActivityPub.create( + :discussion, + %{ + title: title, + text: text, + actor_id: actor_id, + creator_id: creator_id, + attributed_to_id: actor_id + }, + true + ) do + {:ok, discussion} + end + end + + def reply_to_discussion( + _parent, + %{text: text, discussion_id: discussion_id}, + %{ + context: %{ + current_user: %User{} = user + } + } + ) do + with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, + {:no_discussion, + %Discussion{ + actor_id: actor_id, + last_comment: %Comment{ + id: last_comment_id, + origin_comment_id: origin_comment_id, + in_reply_to_comment_id: previous_in_reply_to_comment_id + } + } = _discussion} <- + {:no_discussion, Discussions.get_discussion(discussion_id)}, + {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)}, + {:ok, _activity, %Discussion{} = discussion} <- + ActivityPub.create( + :discussion, + %{ + text: text, + discussion_id: discussion_id, + actor_id: creator_id, + attributed_to_id: actor_id, + in_reply_to_comment_id: last_comment_id, + origin_comment_id: + origin_comment_id || previous_in_reply_to_comment_id || last_comment_id + }, + true + ) do + {:ok, discussion} + end + end + + @spec update_discussion(map(), map(), map()) :: {:ok, Discussion.t()} + def update_discussion( + _parent, + %{title: title, discussion_id: discussion_id}, + %{ + context: %{ + current_user: %User{} = user + } + } + ) do + with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, + {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <- + {:no_discussion, Discussions.get_discussion(discussion_id)}, + {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)}, + {:ok, _activity, %Discussion{} = discussion} <- + ActivityPub.update( + discussion, + %{ + title: title + } + ) do + {:ok, discussion} + end + end + + def delete_discussion(_parent, %{discussion_id: discussion_id}, %{ + context: %{ + current_user: %User{} = user + } + }) do + with {:actor, %Actor{id: creator_id} = actor} <- {:actor, Users.get_actor_for_user(user)}, + {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <- + {:no_discussion, Discussions.get_discussion(discussion_id)}, + {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)}, + {:ok, _activity, %Discussion{} = discussion} <- + ActivityPub.delete(discussion, actor) do + {:ok, discussion} + end + end +end diff --git a/lib/graphql/resolvers/event.ex b/lib/graphql/resolvers/event.ex index 0bf80ee8c..2e974a92a 100644 --- a/lib/graphql/resolvers/event.ex +++ b/lib/graphql/resolvers/event.ex @@ -255,13 +255,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do ) do with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id), {actor_id, ""} <- Integer.parse(actor_id), - {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id) do + {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id) do cond do {:event_can_be_managed, true} == Event.can_be_managed_by(event, actor_id) -> - do_delete_event(event) + do_delete_event(event, actor) role in [:moderator, :administrator] -> - with {:ok, res} <- do_delete_event(event, !is_local), + with {:ok, res} <- do_delete_event(event, actor, !is_local), %Actor{} = actor <- Actors.get_actor(actor_id) do Admin.log_action(actor, "delete", event) @@ -284,8 +284,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do {:error, "You need to be logged-in to delete an event"} end - defp do_delete_event(event, federate \\ true) when is_boolean(federate) do - with {:ok, _activity, event} <- API.Events.delete_event(event) do + defp do_delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) + when is_boolean(federate) do + with {:ok, _activity, event} <- API.Events.delete_event(event, actor) do {:ok, %{id: event.id}} end end diff --git a/lib/graphql/resolvers/group.ex b/lib/graphql/resolvers/group.ex index 1e4fdb7b1..b406fab49 100644 --- a/lib/graphql/resolvers/group.ex +++ b/lib/graphql/resolvers/group.ex @@ -80,7 +80,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do API.Groups.create_group(args) do {:ok, group} else - {:error, err} when is_bitstring(err) -> + {:error, err} when is_binary(err) -> {:error, err} {:is_owned, nil} -> @@ -92,6 +92,36 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do {:error, "You need to be logged-in to create a group"} end + @doc """ + Create a new group. The creator is automatically added as admin + """ + def update_group( + _parent, + args, + %{ + context: %{ + current_user: %User{} = user + } + } + ) do + with %Actor{} = updater_actor <- Users.get_actor_for_user(user), + args <- Map.put(args, :updater_actor, updater_actor), + {:ok, _activity, %Actor{type: :Group} = group} <- + API.Groups.update_group(args) do + {:ok, group} + else + {:error, err} when is_binary(err) -> + {:error, err} + + {:is_owned, nil} -> + {:error, "Creator actor id is not owned by the current user"} + end + end + + def update_group(_parent, _args, _resolution) do + {:error, "You need to be logged-in to update a group"} + end + @doc """ Delete an existing group """ diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex index c29eb3637..ead62878a 100644 --- a/lib/graphql/resolvers/member.ex +++ b/lib/graphql/resolvers/member.ex @@ -16,14 +16,26 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do """ def find_members_for_group( %Actor{id: group_id} = group, - _args, + %{page: page, limit: limit, roles: roles}, %{ context: %{current_user: %User{} = user} } = _resolution ) do with %Actor{id: actor_id} <- Users.get_actor_for_user(user), - {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, - %Page{} = page <- Actors.list_members_for_group(group) do + {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do + roles = + case roles do + "" -> + [] + + roles -> + roles + |> String.split(",") + |> Enum.map(&String.downcase/1) + |> Enum.map(&String.to_existing_atom/1) + end + + %Page{} = page = Actors.list_members_for_group(group, roles, page, limit) {:ok, page} else {:member, false} -> diff --git a/lib/graphql/resolvers/person.ex b/lib/graphql/resolvers/person.ex index 472d86669..edc46fc02 100644 --- a/lib/graphql/resolvers/person.ex +++ b/lib/graphql/resolvers/person.ex @@ -129,7 +129,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do {:find_actor, Actors.get_actor(id)}, {:is_owned, %Actor{}} <- User.owns_actor(user, actor.id), args <- save_attached_pictures(args), - {:ok, _activity, %Actor{} = actor} <- ActivityPub.update(:actor, actor, args, true) do + {:ok, _activity, %Actor{} = actor} <- ActivityPub.update(actor, args, true) do {:ok, actor} else {:find_actor, nil} -> diff --git a/lib/graphql/resolvers/post.ex b/lib/graphql/resolvers/post.ex new file mode 100644 index 000000000..5e1dfa2be --- /dev/null +++ b/lib/graphql/resolvers/post.ex @@ -0,0 +1,198 @@ +defmodule Mobilizon.GraphQL.Resolvers.Post do + @moduledoc """ + Handles the posts-related GraphQL calls + """ + + alias Mobilizon.{Actors, Posts, Users} + alias Mobilizon.Actors.Actor + alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Posts.Post + alias Mobilizon.Storage.Page + alias Mobilizon.Users.User + + require Logger + + @public_accessible_visibilities [:public, :unlisted] + + @doc """ + Find posts for group. + + Returns only if actor requesting is a member of the group + """ + def find_posts_for_group( + %Actor{id: group_id} = group, + %{page: page, limit: limit} = args, + %{ + context: %{ + current_user: %User{} = user + } + } = _resolution + ) do + with %Actor{id: actor_id} <- Users.get_actor_for_user(user), + {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, + %Page{} = page <- Posts.get_posts_for_group(group, page, limit) do + {:ok, page} + else + {:member, _} -> + find_posts_for_group(group, args, nil) + end + end + + def find_posts_for_group( + %Actor{} = group, + %{page: page, limit: limit}, + _resolution + ) do + with %Page{} = page <- Posts.get_public_posts_for_group(group, page, limit) do + {:ok, page} + end + end + + def find_posts_for_group( + _group, + _args, + _resolution + ) do + {:ok, %Page{total: 0, elements: []}} + end + + def get_post( + parent, + %{slug: slug}, + %{ + context: %{ + current_user: %User{} = user + } + } = _resolution + ) do + with {:current_actor, %Actor{id: actor_id}} <- + {:current_actor, Users.get_actor_for_user(user)}, + {:post, %Post{attributed_to: %Actor{id: group_id}} = post} <- + {:post, Posts.get_post_by_slug_with_preloads(slug)}, + {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do + {:ok, post} + else + {:member, false} -> get_post(parent, %{slug: slug}, nil) + {:post, _} -> {:error, "No such post"} + end + end + + def get_post( + _parent, + %{slug: slug}, + _resolution + ) do + case {:post, Posts.get_post_by_slug_with_preloads(slug)} do + {:post, %Post{visibility: visibility, draft: false} = post} + when visibility in @public_accessible_visibilities -> + {:ok, post} + + {:post, _} -> + {:error, "No such post"} + end + end + + def get_post(_parent, _args, _resolution) do + {:error, "No such post"} + end + + def create_post( + _parent, + %{attributed_to_id: group_id} = args, + %{ + context: %{ + current_user: %User{} = user + } + } = _resolution + ) do + with %Actor{id: actor_id} <- Users.get_actor_for_user(user), + {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, + {:ok, _, %Post{} = post} <- + ActivityPub.create( + :post, + args + |> Map.put(:author_id, actor_id) + |> Map.put(:attributed_to_id, group_id), + true, + %{} + ) do + {:ok, post} + else + {:own_check, _} -> + {:error, "Parent post doesn't match this group"} + + {:member, _} -> + {:error, "Actor id is not member of group"} + end + end + + def create_post(_parent, _args, _resolution) do + {:error, "You need to be logged-in to create posts"} + end + + def update_post( + _parent, + %{id: id} = args, + %{ + context: %{ + current_user: %User{} = user + } + } = _resolution + ) do + with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(id)}, + %Actor{id: actor_id} <- Users.get_actor_for_user(user), + {:post, %Post{attributed_to: %Actor{id: group_id}} = post} <- + {:post, Posts.get_post_with_preloads(id)}, + {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, + {:ok, _, %Post{} = post} <- + ActivityPub.update(post, args, true, %{}) do + {:ok, post} + else + {:uuid, :error} -> + {:error, "Post ID is not a valid ID"} + + {:post, _} -> + {:error, "Post doesn't exist"} + + {:member, _} -> + {:error, "Actor id is not member of group"} + end + end + + def update_post(_parent, _args, _resolution) do + {:error, "You need to be logged-in to update posts"} + end + + def delete_post( + _parent, + %{id: post_id}, + %{ + context: %{ + current_user: %User{} = user + } + } = _resolution + ) do + with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(post_id)}, + %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user), + {:post, %Post{attributed_to: %Actor{id: group_id}} = post} <- + {:post, Posts.get_post_with_preloads(post_id)}, + {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, + {:ok, _, %Post{} = post} <- + ActivityPub.delete(post, actor) do + {:ok, post} + else + {:uuid, :error} -> + {:error, "Post ID is not a valid ID"} + + {:post, _} -> + {:error, "Post doesn't exist"} + + {:member, _} -> + {:error, "Actor id is not member of group"} + end + end + + def delete_post(_parent, _args, _resolution) do + {:error, "You need to be logged-in to delete posts"} + end +end diff --git a/lib/graphql/resolvers/resource.ex b/lib/graphql/resolvers/resource.ex index a72db2a4b..5310de03c 100644 --- a/lib/graphql/resolvers/resource.ex +++ b/lib/graphql/resolvers/resource.ex @@ -141,7 +141,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do {:resource, Resources.get_resource_with_preloads(resource_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Resource{} = resource} <- - ActivityPub.update(:resource, resource, args, true, %{}) do + ActivityPub.update(resource, args, true, %{}) do {:ok, resource} else {:resource, _} -> @@ -165,12 +165,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do } } = _resolution ) do - with %Actor{id: actor_id} <- Users.get_actor_for_user(user), + with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user), {:resource, %Resource{parent_id: _parent_id, actor_id: group_id} = resource} <- {:resource, Resources.get_resource_with_preloads(resource_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Resource{} = resource} <- - ActivityPub.delete(resource) do + ActivityPub.delete(resource, actor) do {:ok, resource} else {:resource, _} -> diff --git a/lib/graphql/resolvers/tag.ex b/lib/graphql/resolvers/tag.ex index a21eb7420..82188c139 100644 --- a/lib/graphql/resolvers/tag.ex +++ b/lib/graphql/resolvers/tag.ex @@ -3,8 +3,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do Handles the tag-related GraphQL calls """ - alias Mobilizon.Events + alias Mobilizon.{Events, Posts} alias Mobilizon.Events.{Event, Tag} + alias Mobilizon.Posts.Post def list_tags(_parent, %{page: page, limit: limit}, _resolution) do tags = Mobilizon.Events.list_tags(page, limit) @@ -16,7 +17,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do Retrieve the list of tags for an event """ def list_tags_for_event(%Event{id: id}, _args, _resolution) do - {:ok, Mobilizon.Events.list_tags_for_event(id)} + {:ok, Events.list_tags_for_event(id)} end @doc """ @@ -24,10 +25,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do """ 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)} + {:ok, Events.list_tags_for_event(event_id)} end end + @doc """ + Retrieve the list of tags for a post + """ + def list_tags_for_post(%Post{id: id}, _args, _resolution) do + {:ok, Posts.list_tags_for_post(id)} + end + # @doc """ # Retrieve the list of related tags for a given tag ID # """ @@ -42,7 +50,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do Retrieve the list of related tags for a parent tag """ def get_related_tags(%Tag{} = tag, _args, _resolution) do - with tags <- Mobilizon.Events.list_tag_neighbors(tag) do + with tags <- Events.list_tag_neighbors(tag) do {:ok, tags} end end diff --git a/lib/graphql/resolvers/todos.ex b/lib/graphql/resolvers/todos.ex index 9f6844b32..73d7ce731 100644 --- a/lib/graphql/resolvers/todos.ex +++ b/lib/graphql/resolvers/todos.ex @@ -211,7 +211,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do {:todo_list, Todos.get_todo_list(todo_list_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:ok, _, %Todo{} = todo} <- - ActivityPub.update(:todo, todo, args, true, %{}) do + ActivityPub.update(todo, args, true, %{}) do {:ok, todo} else {:todo_list, _} -> diff --git a/lib/graphql/resolvers/user.ex b/lib/graphql/resolvers/user.ex index 69414974e..a440b333b 100644 --- a/lib/graphql/resolvers/user.ex +++ b/lib/graphql/resolvers/user.ex @@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do alias Mobilizon.Actors.Actor alias Mobilizon.Crypto alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Service.Auth.Authenticator alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Users.{Setting, User} @@ -417,7 +418,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do with {:moderator_actor, %Actor{} = moderator_actor} <- {:moderator_actor, Users.get_actor_for_user(moderator_user)}, %User{disabled: false} = user <- Users.get_user(user_id), - {:ok, %User{}} <- do_delete_account(%User{} = user) do + {:ok, %User{}} <- + do_delete_account(%User{} = user, Relay.get_actor()) do Admin.log_action(moderator_actor, "delete", user) else {:moderator_actor, nil} -> @@ -432,7 +434,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do {:error, "You need to be logged-in to delete your account"} end - defp do_delete_account(%User{} = user) do + defp do_delete_account(%User{} = user, actor_performing \\ nil) do with actors <- Users.get_actors_for_user(user), activated <- not is_nil(user.confirmed_at), # Detach actors from user @@ -444,7 +446,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do # Launch a background job to delete actors :ok <- Enum.each(actors, fn actor -> - ActivityPub.delete(actor, true) + actor_performing = actor_performing || actor + ActivityPub.delete(actor, actor_performing, true) end), # Delete user {:ok, user} <- Users.delete_user(user, reserve_email: activated) do diff --git a/lib/graphql/schema.ex b/lib/graphql/schema.ex index 34fe0c309..545605d73 100644 --- a/lib/graphql/schema.ex +++ b/lib/graphql/schema.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema do alias Mobilizon.{ Actors, Addresses, - Conversations, + Discussions, Events, Media, Reports, @@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.Schema do } alias Mobilizon.Actors.{Actor, Follower, Member} - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.{Event, Participant} alias Mobilizon.GraphQL.Schema alias Mobilizon.Storage.Repo @@ -34,10 +34,11 @@ defmodule Mobilizon.GraphQL.Schema do import_types(Schema.Actors.PersonType) import_types(Schema.Actors.GroupType) import_types(Schema.Actors.ApplicationType) - import_types(Schema.Conversations.CommentType) - import_types(Schema.Conversations.ConversationType) + import_types(Schema.Discussions.CommentType) + import_types(Schema.Discussions.DiscussionType) import_types(Schema.SearchType) import_types(Schema.ResourceType) + import_types(Schema.PostType) import_types(Schema.Todos.TodoListType) import_types(Schema.Todos.TodoType) import_types(Schema.ConfigType) @@ -116,7 +117,7 @@ defmodule Mobilizon.GraphQL.Schema do |> Dataloader.add_source(Actors, default_source) |> Dataloader.add_source(Users, default_source) |> Dataloader.add_source(Events, default_source) - |> Dataloader.add_source(Conversations, Conversations.data()) + |> Dataloader.add_source(Discussions, Discussions.data()) |> Dataloader.add_source(Addresses, default_source) |> Dataloader.add_source(Media, default_source) |> Dataloader.add_source(Reports, default_source) @@ -148,8 +149,9 @@ defmodule Mobilizon.GraphQL.Schema do import_fields(:admin_queries) import_fields(:todo_list_queries) import_fields(:todo_queries) - import_fields(:conversation_queries) + import_fields(:discussion_queries) import_fields(:resource_queries) + import_fields(:post_queries) import_fields(:statistics_queries) end @@ -170,8 +172,9 @@ defmodule Mobilizon.GraphQL.Schema do import_fields(:admin_mutations) import_fields(:todo_list_mutations) import_fields(:todo_mutations) - import_fields(:conversation_mutations) + import_fields(:discussion_mutations) import_fields(:resource_mutations) + import_fields(:post_mutations) end @desc """ @@ -179,5 +182,6 @@ defmodule Mobilizon.GraphQL.Schema do """ subscription do import_fields(:person_subscriptions) + import_fields(:discussion_subscriptions) end end diff --git a/lib/graphql/schema/actors/group.ex b/lib/graphql/schema/actors/group.ex index a74a7382c..18f24af4d 100644 --- a/lib/graphql/schema/actors/group.ex +++ b/lib/graphql/schema/actors/group.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do use Absinthe.Schema.Notation - alias Mobilizon.GraphQL.Resolvers.{Conversation, Group, Member, Resource, Todos} + alias Mobilizon.GraphQL.Resolvers.{Discussion, Group, Member, Post, Resource, Todos} alias Mobilizon.GraphQL.Schema import_types(Schema.Actors.MemberType) @@ -46,9 +46,9 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do description("A list of the events this actor has organized") end - field :conversations, :paginated_conversation_list do - resolve(&Conversation.find_conversations_for_actor/3) - description("A list of the conversations for this group") + field :discussions, :paginated_discussion_list do + resolve(&Discussion.find_discussions_for_actor/3) + description("A list of the discussions for this group") end field(:types, :group_type, description: "The type of group : Group, Community,…") @@ -58,8 +58,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do ) field :members, :paginated_member_list do + arg(:page, :integer, default_value: 1) + arg(:limit, :integer, default_value: 10) + arg(:roles, :string, default_value: "") resolve(&Member.find_members_for_group/3) - description("List of group members") + description("A paginated list of group members") end field :resources, :paginated_resource_list do @@ -69,6 +72,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do description("A paginated list of the resources this group has") end + field :posts, :paginated_post_list do + arg(:page, :integer, default_value: 1) + arg(:limit, :integer, default_value: 10) + resolve(&Post.find_posts_for_group/3) + description("A paginated list of the posts this group has") + end + field :todo_lists, :paginated_todo_list_list do resolve(&Todos.find_todo_lists_for_group/3) description("A paginated list of the todo lists this group has") @@ -99,6 +109,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do field(:total, :integer, description: "The total number of elements in the list") end + @desc "The list of visibility options for a group" + enum :group_visibility do + value(:public, description: "Publicly listed and federated") + value(:unlisted, description: "Visible only to people with the link - or invited") + end + object :group_queries do @desc "Get all groups" field :groups, :paginated_group_list do @@ -124,6 +140,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do arg(:name, :string, description: "The displayed name for the group") arg(:summary, :string, description: "The summary for the group", default_value: "") + arg(:visibility, :group_visibility, + description: "The visibility for the group", + default_value: :public + ) + arg(:avatar, :picture_input, description: "The avatar for the group, either as an object or directly the ID of an existing Picture" @@ -137,6 +158,26 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do resolve(&Group.create_group/3) end + @desc "Update a group" + field :update_group, :group do + arg(:id, non_null(:id), description: "The group ID") + + arg(:name, :string, description: "The displayed name for the group") + arg(:summary, :string, description: "The summary for the group", default_value: "") + + arg(:avatar, :picture_input, + description: + "The avatar for the group, either as an object or directly the ID of an existing Picture" + ) + + arg(:banner, :picture_input, + description: + "The banner for the group, either as an object or directly the ID of an existing Picture" + ) + + resolve(&Group.update_group/3) + end + @desc "Delete a group" field :delete_group, :deleted_object do arg(:group_id, non_null(:id)) diff --git a/lib/graphql/schema/actors/member.ex b/lib/graphql/schema/actors/member.ex index 89dcac778..c08e0dd9d 100644 --- a/lib/graphql/schema/actors/member.ex +++ b/lib/graphql/schema/actors/member.ex @@ -15,6 +15,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do field(:actor, :person, description: "Which profile is member of") field(:role, :member_role_enum, description: "The role of this membership") field(:invited_by, :person, description: "Who invited this member") + field(:inserted_at, :naive_datetime, description: "When was this member created") end enum :member_role_enum do diff --git a/lib/graphql/schema/admin.ex b/lib/graphql/schema/admin.ex index 2d85474a1..c9f1695af 100644 --- a/lib/graphql/schema/admin.ex +++ b/lib/graphql/schema/admin.ex @@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do use Absinthe.Schema.Notation alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Users.User diff --git a/lib/graphql/schema/conversations/conversation.ex b/lib/graphql/schema/conversations/conversation.ex deleted file mode 100644 index 307a90fba..000000000 --- a/lib/graphql/schema/conversations/conversation.ex +++ /dev/null @@ -1,74 +0,0 @@ -defmodule Mobilizon.GraphQL.Schema.Conversations.ConversationType do - @moduledoc """ - Schema representation for Conversation - """ - use Absinthe.Schema.Notation - - import Absinthe.Resolution.Helpers, only: [dataloader: 1] - - alias Mobilizon.Actors - alias Mobilizon.GraphQL.Resolvers.Conversation - - @desc "A conversation" - object :conversation do - field(:id, :id, description: "Internal ID for this conversation") - field(:title, :string) - field(:slug, :string) - field(:last_comment, :comment) - - field :comments, :paginated_comment_list do - arg(:page, :integer, default_value: 1) - arg(:limit, :integer, default_value: 10) - resolve(&Conversation.get_comments_for_conversation/3) - description("The comments for the conversation") - end - - field(:creator, :person, resolve: dataloader(Actors)) - field(:actor, :actor, resolve: dataloader(Actors)) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) - end - - object :paginated_conversation_list do - field(:elements, list_of(:conversation), description: "A list of conversation") - field(:total, :integer, description: "The total number of comments in the list") - end - - object :conversation_queries do - @desc "Get a conversation" - field :conversation, type: :conversation do - arg(:id, non_null(:id)) - resolve(&Conversation.get_conversation/3) - end - end - - object :conversation_mutations do - @desc "Create a conversation" - field :create_conversation, type: :conversation do - arg(:title, non_null(:string)) - arg(:text, non_null(:string)) - arg(:actor_id, non_null(:id)) - arg(:creator_id, non_null(:id)) - - resolve(&Conversation.create_conversation/3) - end - - field :reply_to_conversation, type: :conversation do - arg(:conversation_id, non_null(:id)) - arg(:text, non_null(:string)) - resolve(&Conversation.reply_to_conversation/3) - end - - field :update_conversation, type: :conversation do - arg(:title, non_null(:string)) - arg(:conversation_id, non_null(:id)) - resolve(&Conversation.update_conversation/3) - end - - field :delete_conversation, type: :conversation do - arg(:conversation_id, non_null(:id)) - - # resolve(&Conversation.delete_conversation/3) - end - end -end diff --git a/lib/graphql/schema/conversations/comment.ex b/lib/graphql/schema/discussions/comment.ex similarity index 89% rename from lib/graphql/schema/conversations/comment.ex rename to lib/graphql/schema/discussions/comment.ex index c6f7911ed..8f85e331e 100644 --- a/lib/graphql/schema/conversations/comment.ex +++ b/lib/graphql/schema/discussions/comment.ex @@ -1,4 +1,4 @@ -defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do +defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do @moduledoc """ Schema representation for Comment """ @@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do import Absinthe.Resolution.Helpers, only: [dataloader: 1] - alias Mobilizon.{Actors, Conversations} + alias Mobilizon.{Actors, Discussions} alias Mobilizon.GraphQL.Resolvers.Comment @desc "A comment" @@ -21,13 +21,13 @@ defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do field(:primaryLanguage, :string) field(:replies, list_of(:comment)) do - resolve(dataloader(Conversations)) + resolve(dataloader(Discussions)) end field(:total_replies, :integer) - field(:in_reply_to_comment, :comment, resolve: dataloader(Conversations)) + field(:in_reply_to_comment, :comment, resolve: dataloader(Discussions)) field(:event, :event, resolve: dataloader(Events)) - field(:origin_comment, :comment, resolve: dataloader(Conversations)) + field(:origin_comment, :comment, resolve: dataloader(Discussions)) field(:threadLanguages, non_null(list_of(:string))) field(:actor, :person, resolve: dataloader(Actors)) field(:inserted_at, :datetime) diff --git a/lib/graphql/schema/discussions/discussion.ex b/lib/graphql/schema/discussions/discussion.ex new file mode 100644 index 000000000..020339aae --- /dev/null +++ b/lib/graphql/schema/discussions/discussion.ex @@ -0,0 +1,85 @@ +defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do + @moduledoc """ + Schema representation for discussion + """ + use Absinthe.Schema.Notation + + import Absinthe.Resolution.Helpers, only: [dataloader: 1] + + alias Mobilizon.Actors + alias Mobilizon.GraphQL.Resolvers.Discussion + + @desc "A discussion" + object :discussion do + field(:id, :id, description: "Internal ID for this discussion") + field(:title, :string) + field(:slug, :string) + field(:last_comment, :comment) + + field :comments, :paginated_comment_list do + arg(:page, :integer, default_value: 1) + arg(:limit, :integer, default_value: 10) + resolve(&Discussion.get_comments_for_discussion/3) + description("The comments for the discussion") + end + + field(:creator, :person, resolve: dataloader(Actors)) + field(:actor, :actor, resolve: dataloader(Actors)) + field(:inserted_at, :datetime) + field(:updated_at, :datetime) + end + + object :paginated_discussion_list do + field(:elements, list_of(:discussion), description: "A list of discussion") + field(:total, :integer, description: "The total number of comments in the list") + end + + object :discussion_queries do + @desc "Get a discussion" + field :discussion, type: :discussion do + arg(:id, :id) + arg(:slug, :string) + resolve(&Discussion.get_discussion/3) + end + end + + object :discussion_mutations do + @desc "Create a discussion" + field :create_discussion, type: :discussion do + arg(:title, non_null(:string)) + arg(:text, non_null(:string)) + arg(:actor_id, non_null(:id)) + arg(:creator_id, non_null(:id)) + + resolve(&Discussion.create_discussion/3) + end + + field :reply_to_discussion, type: :discussion do + arg(:discussion_id, non_null(:id)) + arg(:text, non_null(:string)) + resolve(&Discussion.reply_to_discussion/3) + end + + field :update_discussion, type: :discussion do + arg(:title, non_null(:string)) + arg(:discussion_id, non_null(:id)) + resolve(&Discussion.update_discussion/3) + end + + field :delete_discussion, type: :discussion do + arg(:discussion_id, non_null(:id)) + + resolve(&Discussion.delete_discussion/3) + end + end + + object :discussion_subscriptions do + field :discussion_comment_changed, :discussion do + arg(:slug, non_null(:string)) + + config(fn args, _ -> + {:ok, topic: args.slug} + end) + end + end +end diff --git a/lib/graphql/schema/event.ex b/lib/graphql/schema/event.ex index 9b93a94aa..d6a0ec56b 100644 --- a/lib/graphql/schema/event.ex +++ b/lib/graphql/schema/event.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do import Absinthe.Resolution.Helpers, only: [dataloader: 1] import Mobilizon.GraphQL.Helpers.Error - alias Mobilizon.{Actors, Addresses, Conversations} + alias Mobilizon.{Actors, Addresses, Discussions} alias Mobilizon.GraphQL.Resolvers.{Event, Picture, Tag} alias Mobilizon.GraphQL.Schema @@ -82,7 +82,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do ) field(:comments, list_of(:comment), description: "The comments in reply to the event") do - resolve(dataloader(Conversations)) + resolve(dataloader(Discussions)) end # field(:tracks, list_of(:track)) diff --git a/lib/graphql/schema/post.ex b/lib/graphql/schema/post.ex new file mode 100644 index 000000000..fdfb61bcb --- /dev/null +++ b/lib/graphql/schema/post.ex @@ -0,0 +1,91 @@ +defmodule Mobilizon.GraphQL.Schema.PostType do + @moduledoc """ + Schema representation for Posts + """ + use Absinthe.Schema.Notation + alias Mobilizon.GraphQL.Resolvers.{Post, Tag} + + @desc "A post" + object :post do + field(:id, :id, description: "The post's ID") + field(:title, :string, description: "The post's title") + field(:slug, :string, description: "The post's slug") + field(:body, :string, description: "The post's body, as HTML") + field(:url, :string, description: "The post's URL") + field(:draft, :boolean, description: "Whether the post is a draft") + field(:author, :actor, description: "The post's author") + field(:attributed_to, :actor, description: "The post's group") + field(:visibility, :post_visibility, description: "The post's visibility") + field(:publish_at, :datetime, description: "When the post was published") + field(:inserted_at, :naive_datetime, description: "The post's creation date") + field(:updated_at, :naive_datetime, description: "The post's last update date") + + field(:tags, list_of(:tag), + resolve: &Tag.list_tags_for_post/3, + description: "The post's tags" + ) + end + + object :paginated_post_list do + field(:elements, list_of(:post), description: "A list of posts") + field(:total, :integer, description: "The total number of posts in the list") + end + + @desc "The list of visibility options for a post" + enum :post_visibility do + value(:public, description: "Publicly listed and federated. Can be shared.") + value(:unlisted, description: "Visible only to people with the link") + # value(:restricted, description: "Visible only after a moderator accepted") + + value(:private, + description: "Visible only to people members of the group or followers of the person" + ) + end + + object :post_queries do + @desc "Get a post" + field :post, :post do + arg(:slug, non_null(:string)) + resolve(&Post.get_post/3) + end + end + + object :post_mutations do + @desc "Create a post" + field :create_post, :post do + arg(:attributed_to_id, non_null(:id)) + arg(:title, non_null(:string)) + arg(:body, :string) + arg(:draft, :boolean, default_value: false) + arg(:visibility, :post_visibility) + arg(:publish_at, :datetime) + + arg(:tags, list_of(:string), + default_value: [], + description: "The list of tags associated to the post" + ) + + resolve(&Post.create_post/3) + end + + @desc "Update a post" + field :update_post, :post do + arg(:id, non_null(:id)) + arg(:title, :string) + arg(:body, :string) + arg(:attributed_to_id, :id) + arg(:draft, :boolean) + arg(:visibility, :post_visibility) + arg(:publish_at, :datetime) + arg(:tags, list_of(:string), description: "The list of tags associated to the post") + + resolve(&Post.update_post/3) + end + + @desc "Delete a post" + field :delete_post, :deleted_object do + arg(:id, non_null(:id)) + resolve(&Post.delete_post/3) + end + end +end diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index c84b5d64c..57f866095 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -9,7 +9,7 @@ defmodule Mobilizon.Actors.Actor do alias Mobilizon.{Actors, Config, Crypto, Mention, Share} alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member} - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.{Event, FeedToken} alias Mobilizon.Media.File alias Mobilizon.Reports.{Note, Report} @@ -27,6 +27,9 @@ defmodule Mobilizon.Actors.Actor do following_url: String.t(), followers_url: String.t(), shared_inbox_url: String.t(), + resources_url: String.t(), + posts_url: String.t(), + events_url: String.t(), type: ActorType.t(), name: String.t(), domain: String.t(), @@ -62,6 +65,10 @@ defmodule Mobilizon.Actors.Actor do :shared_inbox_url, :following_url, :followers_url, + :posts_url, + :events_url, + :todos_url, + :discussions_url, :type, :name, :domain, @@ -96,6 +103,10 @@ defmodule Mobilizon.Actors.Actor do :followers_url, :members_url, :resources_url, + :posts_url, + :todos_url, + :events_url, + :discussions_url, :name, :summary, :manually_approves_followers, @@ -117,6 +128,7 @@ defmodule Mobilizon.Actors.Actor do schema "actors" do field(:url, :string) + field(:outbox_url, :string) field(:inbox_url, :string) field(:following_url, :string) @@ -124,7 +136,11 @@ defmodule Mobilizon.Actors.Actor do field(:shared_inbox_url, :string) field(:members_url, :string) field(:resources_url, :string) + field(:posts_url, :string) + field(:events_url, :string) field(:todos_url, :string) + field(:discussions_url, :string) + field(:type, ActorType, default: :Person) field(:name, :string) field(:domain, :string, default: nil) @@ -344,7 +360,8 @@ defmodule Mobilizon.Actors.Actor do def build_url("relay", :page, _args), do: Endpoint |> Routes.activity_pub_url(:relay) |> URI.decode() - def build_url(preferred_username, endpoint, args) when endpoint in [:page, :resources] do + def build_url(preferred_username, endpoint, args) + when endpoint in [:page, :resources, :posts, :discussions, :events, :todos] do endpoint = if endpoint == :page, do: :actor, else: endpoint Endpoint @@ -353,7 +370,7 @@ defmodule Mobilizon.Actors.Actor do end def build_url(preferred_username, endpoint, args) - when endpoint in [:outbox, :following, :followers, :members, :todos] do + when endpoint in [:outbox, :following, :followers, :members] do Endpoint |> Routes.activity_pub_url(endpoint, preferred_username, args) |> URI.decode() diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 757b0eedc..57118cc16 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -55,6 +55,8 @@ defmodule Mobilizon.Actors do @public_visibility [:public, :unlisted] @administrator_roles [:creator, :administrator] + @moderator_roles [:moderator] ++ @administrator_roles + @member_roles [:member] ++ @moderator_roles @actor_preloads [:user, :organized_events, :comments] @doc """ @@ -118,6 +120,17 @@ defmodule Mobilizon.Actors do end end + @doc """ + New function to replace `Mobilizon.Actors.get_actor_by_url/1` with + better signature + """ + @spec get_actor_by_url_2(String.t(), boolean) :: Actor.t() | nil + def get_actor_by_url_2(url, preload \\ false) do + Actor + |> Repo.get_by(url: url) + |> preload_followers(preload) + end + @doc """ Gets an actor by its URL (ActivityPub ID). The `:preload` option allows to preload the followers relation. @@ -181,9 +194,17 @@ defmodule Mobilizon.Actors do """ @spec create_actor(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def create_actor(attrs \\ %{}) do - %Actor{} - |> Actor.changeset(attrs) - |> Repo.insert() + type = Map.get(attrs, :type, :Person) + + case type do + :Person -> + %Actor{} + |> Actor.changeset(attrs) + |> Repo.insert() + + :Group -> + create_group(attrs) + end end @doc """ @@ -238,7 +259,8 @@ defmodule Mobilizon.Actors do name: name, summary: summary, avatar: transform_media_file(avatar), - banner: transform_media_file(banner) + banner: transform_media_file(banner), + last_refreshed_at: DateTime.utc_now() ] ], conflict_target: [:url] @@ -285,6 +307,7 @@ defmodule Mobilizon.Actors do """ @spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def perform(:delete_actor, %Actor{} = actor, options \\ @delete_actor_default_options) do + Logger.info("Going to delete actor #{actor.url}") actor = Repo.preload(actor, @actor_preloads) delete_actor_options = Keyword.merge(@delete_actor_default_options, options) @@ -306,10 +329,18 @@ defmodule Mobilizon.Actors do case Repo.transaction(multi) do {:ok, %{actor: %Actor{} = actor}} -> {:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}") + Logger.info("Deleted actor #{actor.url}") {:ok, actor} {:error, remove, error, _} when remove in [:remove_banner, :remove_avatar] -> + Logger.error("Error while deleting actor's banner or avatar") + Logger.error(inspect(error, pretty: true)) {:error, error} + + err -> + Logger.error("Unknown error while deleting actor") + Logger.error(inspect(err, pretty: true)) + {:error, err} end end @@ -438,23 +469,47 @@ defmodule Mobilizon.Actors do end end + @spec get_local_group_by_url(String.t()) :: Actor.t() + def get_local_group_by_url(group_url) do + group_query() + |> where([q], q.url == ^group_url and is_nil(q.domain)) + |> Repo.one() + end + + @spec get_group_by_members_url(String.t()) :: Actor.t() + def get_group_by_members_url(members_url) do + group_query() + |> where([q], q.members_url == ^members_url) + |> Repo.one() + end + @doc """ Creates a group. + + If the group is local, creates an admin actor as well from `creator_actor_id`. """ @spec create_group(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def create_group(attrs \\ %{}) do - with {:ok, %{insert_group: %Actor{} = group, add_admin_member: %Member{} = _admin_member}} <- - Multi.new() - |> Multi.insert(:insert_group, Actor.group_creation_changeset(%Actor{}, attrs)) - |> Multi.insert(:add_admin_member, fn %{insert_group: group} -> - Member.changeset(%Member{}, %{ - parent_id: group.id, - actor_id: attrs.creator_actor_id, - role: :administrator - }) - end) - |> Repo.transaction() do - {:ok, group} + local = Map.get(attrs, :local, true) + + if local do + with {:ok, %{insert_group: %Actor{} = group, add_admin_member: %Member{} = _admin_member}} <- + Multi.new() + |> Multi.insert(:insert_group, Actor.group_creation_changeset(%Actor{}, attrs)) + |> Multi.insert(:add_admin_member, fn %{insert_group: group} -> + Member.changeset(%Member{}, %{ + parent_id: group.id, + actor_id: attrs.creator_actor_id, + role: :administrator + }) + end) + |> Repo.transaction() do + {:ok, group} + end + else + %Actor{} + |> Actor.group_creation_changeset(attrs) + |> Repo.insert() end end @@ -532,12 +587,7 @@ defmodule Mobilizon.Actors do def is_member?(actor_id, parent_id) do match?( {:ok, %Member{}}, - get_member(actor_id, parent_id, [ - :member, - :moderator, - :administrator, - :creator - ]) + get_member(actor_id, parent_id, @member_roles) ) end @@ -552,6 +602,20 @@ defmodule Mobilizon.Actors do |> Repo.one() end + @spec get_single_group_member_actor(integer() | String.t()) :: Actor.t() | nil + def get_single_group_member_actor(group_id) do + Member + |> where( + [m], + m.parent_id == ^group_id and m.role in [^:member, ^:moderator, ^:administrator, ^:creator] + ) + |> join(:inner, [m], a in Actor, on: m.actor_id == a.id) + |> where([_m, a], is_nil(a.domain)) + |> limit(1) + |> select([_m, a], a) + |> Repo.one() + end + @doc """ Creates a member. """ @@ -616,25 +680,26 @@ defmodule Mobilizon.Actors do @doc """ Returns the list of members for a group. """ - @spec list_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t() - def list_members_for_group(%Actor{id: group_id, type: :Group}, page \\ nil, limit \\ nil) do - group_id - |> members_for_group_query() - |> Page.build_page(page, limit) - end - - @spec list_external_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t() - def list_external_members_for_group( + @spec list_members_for_group(Actor.t(), list(atom()), integer | nil, integer | nil) :: Page.t() + def list_members_for_group( %Actor{id: group_id, type: :Group}, + roles \\ [], page \\ nil, limit \\ nil ) do group_id |> members_for_group_query() - |> filter_external() + |> filter_member_role(roles) |> Page.build_page(page, limit) end + @spec list_external_actors_members_for_group(Actor.t()) :: list(Actor.t()) + def list_external_actors_members_for_group(%Actor{id: group_id, type: :Group}) do + group_id + |> group_external_member_actor_query() + |> Repo.all() + end + @doc """ Returns the list of administrator members for a group. """ @@ -1141,6 +1206,26 @@ defmodule Mobilizon.Actors do ) end + @spec group_external_member_actor_query(integer()) :: Ecto.Query.t() + defp group_external_member_actor_query(group_id) do + Member + |> where([m], m.parent_id == ^group_id) + |> join(:inner, [m], a in Actor, on: m.actor_id == a.id) + |> where([_m, a], not is_nil(a.domain)) + |> select([_m, a], a) + end + + @spec filter_member_role(Ecto.Query.t(), list(atom()) | atom()) :: Ecto.Query.t() + def filter_member_role(query, []), do: query + + def filter_member_role(query, roles) when is_list(roles) do + where(query, [m], m.role in ^roles) + end + + def filter_member_role(query, role) when is_atom(role) do + from(m in query, where: m.role == ^role) + end + @spec administrator_members_for_group_query(integer | String.t()) :: Ecto.Query.t() defp administrator_members_for_group_query(group_id) do from( @@ -1296,13 +1381,22 @@ defmodule Mobilizon.Actors do defp preload_followers(actor, true), do: Repo.preload(actor, [:followers]) defp preload_followers(actor, false), do: actor - defp delete_actor_organized_events(%Actor{organized_events: organized_events}) do + defp delete_actor_organized_events(%Actor{organized_events: organized_events} = actor) do res = Enum.map(organized_events, fn event -> event = - Repo.preload(event, [:organizer_actor, :participants, :picture, :mentions, :comments]) + Repo.preload(event, [ + :organizer_actor, + :participants, + :picture, + :mentions, + :comments, + :attributed_to, + :tags, + :physical_address + ]) - ActivityPub.delete(event, false) + ActivityPub.delete(event, actor, false) end) if Enum.all?(res, fn {status, _, _} -> status == :ok end) do @@ -1312,13 +1406,21 @@ defmodule Mobilizon.Actors do end end - defp delete_actor_empty_comments(%Actor{comments: comments}) do + defp delete_actor_empty_comments(%Actor{comments: comments} = actor) do res = Enum.map(comments, fn comment -> comment = - Repo.preload(comment, [:actor, :mentions, :event, :in_reply_to_comment, :origin_comment]) + Repo.preload(comment, [ + :actor, + :mentions, + :event, + :in_reply_to_comment, + :origin_comment, + :attributed_to, + :tags + ]) - ActivityPub.delete(comment, false) + ActivityPub.delete(comment, actor, false) end) if Enum.all?(res, fn {status, _, _} -> status == :ok end) do diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex index fbe38aeb1..9789d4bd9 100644 --- a/lib/mobilizon/config.ex +++ b/lib/mobilizon/config.ex @@ -119,7 +119,7 @@ defmodule Mobilizon.Config do @spec instance_user_agent :: String.t() def instance_user_agent, - do: "#{instance_name()} #{instance_hostname()} - Mobilizon #{instance_version()}" + do: "#{instance_hostname()} - Mobilizon #{instance_version()}" @spec instance_federating :: String.t() def instance_federating, do: instance_config()[:federating] diff --git a/lib/mobilizon/conversations/conversation.ex b/lib/mobilizon/conversations/conversation.ex deleted file mode 100644 index 1a54271cf..000000000 --- a/lib/mobilizon/conversations/conversation.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Mobilizon.Conversations.Conversation.TitleSlug do - @moduledoc """ - Module to generate the slug for conversations - """ - use EctoAutoslugField.Slug, from: :title, to: :slug -end - -defmodule Mobilizon.Conversations.Conversation do - @moduledoc """ - Represents a conversation - """ - - use Ecto.Schema - - import Ecto.Changeset - - alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment - alias Mobilizon.Conversations.Conversation.TitleSlug - - @type t :: %__MODULE__{ - creator: Actor.t(), - actor: Actor.t(), - title: String.t(), - slug: String.t(), - last_comment: Comment.t(), - comments: list(Comment.t()) - } - - @required_attrs [:actor_id, :creator_id, :title, :last_comment_id] - @optional_attrs [] - @attrs @required_attrs ++ @optional_attrs - - schema "conversations" do - field(:title, :string) - field(:slug, TitleSlug.Type) - belongs_to(:creator, Actor) - belongs_to(:actor, Actor) - belongs_to(:last_comment, Comment) - has_many(:comments, Comment, foreign_key: :conversation_id) - - timestamps(type: :utc_datetime) - end - - @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%__MODULE__{} = conversation, attrs) do - conversation - |> cast(attrs, @attrs) - |> validate_required(@required_attrs) - |> TitleSlug.maybe_generate_slug() - end -end diff --git a/lib/mobilizon/conversations/comment.ex b/lib/mobilizon/discussions/comment.ex similarity index 94% rename from lib/mobilizon/conversations/comment.ex rename to lib/mobilizon/discussions/comment.ex index 2132563f6..bde06e667 100644 --- a/lib/mobilizon/conversations/comment.ex +++ b/lib/mobilizon/discussions/comment.ex @@ -1,4 +1,4 @@ -defmodule Mobilizon.Conversations.Comment do +defmodule Mobilizon.Discussions.Comment do @moduledoc """ Represents an actor comment (for instance on an event or on a group). """ @@ -8,7 +8,7 @@ defmodule Mobilizon.Conversations.Comment do import Ecto.Changeset alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.{Comment, CommentVisibility, Conversation} + alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion} alias Mobilizon.Events.{Event, Tag} alias Mobilizon.Mention @@ -42,7 +42,7 @@ defmodule Mobilizon.Conversations.Comment do :attributed_to_id, :deleted_at, :local, - :conversation_id + :discussion_id ] @attrs @required_attrs ++ @optional_attrs @@ -60,7 +60,7 @@ defmodule Mobilizon.Conversations.Comment do belongs_to(:event, Event, foreign_key: :event_id) belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id) belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id) - belongs_to(:conversation, Conversation) + belongs_to(:discussion, Discussion, type: :binary_id) has_many(:replies, Comment, foreign_key: :in_reply_to_comment_id) many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete) has_many(:mentions, Mention) @@ -69,7 +69,7 @@ defmodule Mobilizon.Conversations.Comment do end @doc """ - Returns the id of the first comment in the conversation. + Returns the id of the first comment in the discussion. """ @spec get_thread_id(t) :: integer def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do @@ -98,6 +98,7 @@ defmodule Mobilizon.Conversations.Comment do |> change() |> put_change(:text, nil) |> put_change(:actor_id, nil) + |> put_change(:discussion_id, nil) |> put_change(:deleted_at, DateTime.utc_now() |> DateTime.truncate(:second)) end diff --git a/lib/mobilizon/discussions/discussion.ex b/lib/mobilizon/discussions/discussion.ex new file mode 100644 index 000000000..8e06b3294 --- /dev/null +++ b/lib/mobilizon/discussions/discussion.ex @@ -0,0 +1,102 @@ +defmodule Mobilizon.Discussions.Discussion.TitleSlug do + @moduledoc """ + Module to generate the slug for discussions + """ + use EctoAutoslugField.Slug, from: [:title, :id], to: :slug + + def build_slug([title, id], %Ecto.Changeset{valid?: true}) do + [title, ShortUUID.encode!(id)] + |> Enum.join("-") + |> Slugger.slugify() + end + + def build_slug(_sources, %Ecto.Changeset{valid?: false}), do: "" +end + +defmodule Mobilizon.Discussions.Discussion do + @moduledoc """ + Represents a discussion + """ + + use Ecto.Schema + + import Ecto.Changeset + + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions.Comment + alias Mobilizon.Discussions.Discussion.TitleSlug + alias Mobilizon.Web.Endpoint + alias Mobilizon.Web.Router.Helpers, as: Routes + + @type t :: %__MODULE__{ + creator: Actor.t(), + actor: Actor.t(), + title: String.t(), + url: String.t(), + slug: String.t(), + last_comment: Comment.t(), + comments: list(Comment.t()) + } + + @required_attrs [:actor_id, :creator_id, :title, :last_comment_id, :url, :id] + @optional_attrs [] + @attrs @required_attrs ++ @optional_attrs + + @primary_key {:id, Ecto.UUID, autogenerate: true} + + schema "discussions" do + field(:title, :string) + field(:slug, TitleSlug.Type) + field(:url, :string) + belongs_to(:creator, Actor) + belongs_to(:actor, Actor) + belongs_to(:last_comment, Comment) + has_many(:comments, Comment, foreign_key: :discussion_id) + + timestamps(type: :utc_datetime) + end + + @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = discussion, attrs) do + discussion + |> cast(attrs, @attrs) + |> maybe_generate_id() + |> validate_required([:title, :id]) + |> TitleSlug.maybe_generate_slug() + |> TitleSlug.unique_constraint() + |> maybe_generate_url() + |> validate_required(@required_attrs) + end + + defp maybe_generate_id(%Ecto.Changeset{} = changeset) do + case fetch_field(changeset, :id) do + res when res in [:error, {:data, nil}] -> + put_change(changeset, :id, Ecto.UUID.generate()) + + _ -> + changeset + end + end + + @spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() + defp maybe_generate_url(%Ecto.Changeset{} = changeset) do + with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url), + {changes, slug} when changes in [:changes, :data] <- + fetch_field(changeset, :slug), + {_changes, actor_id} <- + fetch_field(changeset, :actor_id), + %Actor{preferred_username: preferred_username} <- + Actors.get_actor(actor_id), + url <- generate_url(preferred_username, slug) do + put_change(changeset, :url, url) + else + _ -> changeset + end + end + + @spec generate_url(String.t(), String.t()) :: String.t() + defp generate_url(preferred_username, slug), + do: Routes.page_url(Endpoint, :discussion, preferred_username, slug) +end diff --git a/lib/mobilizon/conversations/conversations.ex b/lib/mobilizon/discussions/discussions.ex similarity index 68% rename from lib/mobilizon/conversations/conversations.ex rename to lib/mobilizon/discussions/discussions.ex index 7bed61f60..d5671e2ba 100644 --- a/lib/mobilizon/conversations/conversations.ex +++ b/lib/mobilizon/discussions/discussions.ex @@ -1,6 +1,6 @@ -defmodule Mobilizon.Conversations do +defmodule Mobilizon.Discussions do @moduledoc """ - The conversations context + The discussions context """ import EctoEnum @@ -9,7 +9,7 @@ defmodule Mobilizon.Conversations do alias Ecto.Changeset alias Ecto.Multi alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.{Comment, Conversation} + alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Storage.{Page, Repo} defenum( @@ -42,10 +42,11 @@ defmodule Mobilizon.Conversations do :origin_comment, :replies, :tags, - :mentions + :mentions, + :discussion ] - @conversation_preloads [ + @discussion_preloads [ :last_comment, :comments, :creator, @@ -231,21 +232,11 @@ defmodule Mobilizon.Conversations do @doc """ Returns the list of public comments for the actor. """ - @spec list_public_comments_for_actor(Actor.t(), integer | nil, integer | nil) :: - {:ok, [Comment.t()], integer} + @spec list_public_comments_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t() def list_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do - comments = - actor_id - |> public_comments_for_actor_query() - |> Page.paginate(page, limit) - |> Repo.all() - - count_comments = - actor_id - |> count_comments_query() - |> Repo.one() - - {:ok, comments, count_comments} + actor_id + |> public_comments_for_actor_query() + |> Page.build_page(page, limit) end @doc """ @@ -263,10 +254,10 @@ defmodule Mobilizon.Conversations do |> Repo.all() end - @spec get_comments_for_conversation(integer, integer | nil, integer | nil) :: Page.t() - def get_comments_for_conversation(conversation_id, page \\ nil, limit \\ nil) do + @spec get_comments_for_discussion(integer, integer | nil, integer | nil) :: Page.t() + def get_comments_for_discussion(discussion_id, page \\ nil, limit \\ nil) do Comment - |> where([c], c.conversation_id == ^conversation_id) + |> where([c], c.discussion_id == ^discussion_id) |> order_by(asc: :inserted_at) |> Page.build_page(page, limit) end @@ -277,80 +268,114 @@ defmodule Mobilizon.Conversations do @spec count_local_comments :: integer def count_local_comments, do: Repo.one(count_local_comments_query()) - def get_conversation(conversation_id) do - Conversation - |> Repo.get(conversation_id) - |> Repo.preload(@conversation_preloads) + def get_discussion(discussion_id) do + Discussion + |> Repo.get(discussion_id) + |> Repo.preload(@discussion_preloads) end - @spec find_conversations_for_actor(integer, integer | nil, integer | nil) :: Page.t() - def find_conversations_for_actor(actor_id, page \\ nil, limit \\ nil) do - Conversation + @spec get_discussion_by_url(String.t() | nil) :: Discussion.t() | nil + def get_discussion_by_url(nil), do: nil + + def get_discussion_by_url(discussion_url) do + Discussion + |> Repo.get_by(url: discussion_url) + |> Repo.preload(@discussion_preloads) + end + + def get_discussion_by_slug(discussion_slug) do + Discussion + |> Repo.get_by(slug: discussion_slug) + |> Repo.preload(@discussion_preloads) + end + + @spec find_discussions_for_actor(integer, integer | nil, integer | nil) :: Page.t() + def find_discussions_for_actor(actor_id, page \\ nil, limit \\ nil) do + Discussion |> where([c], c.actor_id == ^actor_id) - |> preload(^@conversation_preloads) + |> preload(^@discussion_preloads) |> Page.build_page(page, limit) end @doc """ - Creates a conversation. + Creates a discussion. """ - @spec create_conversation(map) :: {:ok, Comment.t()} | {:error, Changeset.t()} - def create_conversation(attrs \\ %{}) do - with {:ok, %{comment: %Comment{} = _comment, conversation: %Conversation{} = conversation}} <- + @spec create_discussion(map) :: {:ok, Comment.t()} | {:error, Changeset.t()} + def create_discussion(attrs \\ %{}) do + with {:ok, %{comment: %Comment{} = _comment, discussion: %Discussion{} = discussion}} <- Multi.new() |> Multi.insert( :comment, - Comment.changeset(%Comment{}, Map.merge(attrs, %{actor_id: attrs.creator_id})) + Comment.changeset( + %Comment{}, + Map.merge(attrs, %{actor_id: attrs.creator_id, attributed_to_id: attrs.actor_id}) + ) ) - |> Multi.insert(:conversation, fn %{comment: %Comment{id: comment_id}} -> - Conversation.changeset( - %Conversation{}, + |> Multi.insert(:discussion, fn %{comment: %Comment{id: comment_id}} -> + Discussion.changeset( + %Discussion{}, Map.merge(attrs, %{last_comment_id: comment_id}) ) end) - |> Multi.update(:comment_conversation, fn %{ - comment: %Comment{} = comment, - conversation: %Conversation{ - id: conversation_id - } - } -> - Changeset.change(comment, %{conversation_id: conversation_id}) + |> Multi.update(:comment_discussion, fn %{ + comment: %Comment{} = comment, + discussion: %Discussion{ + id: discussion_id, + url: discussion_url + } + } -> + Changeset.change(comment, %{discussion_id: discussion_id, url: discussion_url}) end) |> Repo.transaction() do - {:ok, conversation} + {:ok, discussion} end end - def reply_to_conversation(%Conversation{id: conversation_id} = conversation, attrs \\ %{}) do - with {:ok, %{comment: %Comment{} = comment, conversation: %Conversation{} = conversation}} <- + def reply_to_discussion(%Discussion{id: discussion_id} = discussion, attrs \\ %{}) do + with {:ok, %{comment: %Comment{} = comment, discussion: %Discussion{} = discussion}} <- Multi.new() |> Multi.insert( :comment, - Comment.changeset(%Comment{}, Map.merge(attrs, %{conversation_id: conversation_id})) + Comment.changeset( + %Comment{}, + Map.merge(attrs, %{ + discussion_id: discussion_id, + actor_id: Map.get(attrs, :creator_id, attrs.actor_id) + }) + ) ) - |> Multi.update(:conversation, fn %{comment: %Comment{id: comment_id}} -> - Conversation.changeset( - conversation, + |> Multi.update(:discussion, fn %{comment: %Comment{id: comment_id}} -> + Discussion.changeset( + discussion, %{last_comment_id: comment_id} ) end) |> Repo.transaction() do - # For some reason conversation is not updated - {:ok, Map.put(conversation, :last_comment, comment)} + # Discussion is not updated + {:ok, Map.put(discussion, :last_comment, comment)} end end @doc """ - Update a conversation. Only their title for now. + Update a discussion. Only their title for now. """ - @spec update_conversation(Conversation.t(), map()) :: - {:ok, Conversation.t()} | {:error, Changeset.t()} - def update_conversation(%Conversation{} = conversation, attrs \\ %{}) do - conversation - |> Conversation.changeset(attrs) + @spec update_discussion(Discussion.t(), map()) :: + {:ok, Discussion.t()} | {:error, Changeset.t()} + def update_discussion(%Discussion{} = discussion, attrs \\ %{}) do + discussion + |> Discussion.changeset(attrs) |> Repo.update() end + @doc """ + Delete a discussion. + """ + @spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()} + def delete_discussion(%Discussion{} = discussion) do + discussion + |> Repo.delete() + end + defp public_comments_for_actor_query(actor_id) do Comment |> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility) @@ -365,11 +390,6 @@ defmodule Mobilizon.Conversations do |> preload_for_comment() end - @spec count_comments_query(integer) :: Ecto.Query.t() - defp count_comments_query(actor_id) do - from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id) - end - @spec count_local_comments_query :: Ecto.Query.t() defp count_local_comments_query do from( @@ -382,6 +402,6 @@ defmodule Mobilizon.Conversations do @spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t() defp preload_for_comment(query), do: preload(query, ^@comment_preloads) - # @spec preload_for_conversation(Ecto.Query.t()) :: Ecto.Query.t() - # defp preload_for_conversation(query), do: preload(query, ^@conversation_preloads) + # @spec preload_for_discussion(Ecto.Query.t()) :: Ecto.Query.t() + # defp preload_for_discussion(query), do: preload(query, ^@discussion_preloads) end diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index d2893a906..913c730f0 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -13,7 +13,7 @@ defmodule Mobilizon.Events.Event do alias Mobilizon.{Addresses, Events, Media, Mention} alias Mobilizon.Addresses.Address - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.{ EventOptions, diff --git a/lib/mobilizon/events/event_options.ex b/lib/mobilizon/events/event_options.ex index 22abdc9d2..6f1f5832c 100644 --- a/lib/mobilizon/events/event_options.ex +++ b/lib/mobilizon/events/event_options.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Events.EventOptions do import Ecto.Changeset - alias Mobilizon.Conversations.CommentModeration + alias Mobilizon.Discussions.CommentModeration alias Mobilizon.Events.{ EventOffer, diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index f9f01e6e5..a0e62344b 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -380,24 +380,19 @@ defmodule Mobilizon.Events do @doc """ Lists public events for the actor, with all associations loaded. """ - @spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) :: - {:ok, [Event.t()], integer} - def list_public_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do - events = - actor_id - |> event_for_actor_query() - |> filter_public_visibility() - |> filter_draft() - |> preload_for_event() - |> Page.paginate(page, limit) - |> Repo.all() + @spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t() + def list_public_events_for_actor(actor, page \\ nil, limit \\ nil) - events_count = - actor_id - |> count_events_for_actor_query() - |> Repo.one() + def list_public_events_for_actor(%Actor{type: :Group} = group, page, limit), + do: list_organized_events_for_group(group, page, limit) - {:ok, events, events_count} + def list_public_events_for_actor(%Actor{id: actor_id}, page, limit) do + actor_id + |> event_for_actor_query() + |> filter_public_visibility() + |> filter_draft() + |> preload_for_event() + |> Page.build_page(page, limit) end @spec list_organized_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t() @@ -1321,15 +1316,6 @@ defmodule Mobilizon.Events do ) end - @spec count_events_for_actor_query(integer | String.t()) :: Ecto.Query.t() - defp count_events_for_actor_query(actor_id) do - from( - e in Event, - select: count(e.id), - where: e.organizer_actor_id == ^actor_id - ) - end - @spec count_local_events_query :: Ecto.Query.t() defp count_local_events_query do from(e in Event, select: count(e.id), where: e.local == ^true) diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex index a0885d1f6..1670e786e 100644 --- a/lib/mobilizon/events/participant.ex +++ b/lib/mobilizon/events/participant.ex @@ -19,7 +19,7 @@ defmodule Mobilizon.Events.Participant do url: String.t(), event: Event.t(), actor: Actor.t(), - metadata: Map.t() + metadata: map() } @required_attrs [:url, :role, :event_id, :actor_id] diff --git a/lib/mobilizon/mentions/mention.ex b/lib/mobilizon/mentions/mention.ex index 4428e2e21..a1473d22e 100644 --- a/lib/mobilizon/mentions/mention.ex +++ b/lib/mobilizon/mentions/mention.ex @@ -6,7 +6,7 @@ defmodule Mobilizon.Mention do use Ecto.Schema import Ecto.Changeset alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Storage.Repo diff --git a/lib/mobilizon/posts/post.ex b/lib/mobilizon/posts/post.ex new file mode 100644 index 000000000..66d5a20eb --- /dev/null +++ b/lib/mobilizon/posts/post.ex @@ -0,0 +1,137 @@ +defmodule Mobilizon.Posts.Post.TitleSlug do + @moduledoc """ + Module to generate the slug for posts + """ + use EctoAutoslugField.Slug, from: [:title, :id], to: :slug + + def build_slug([title, id], %Ecto.Changeset{valid?: true}) do + [title, ShortUUID.encode!(id)] + |> Enum.join("-") + |> Slugger.slugify() + end + + def build_slug(_sources, %Ecto.Changeset{valid?: false}), do: "" +end + +defmodule Mobilizon.Posts.Post do + @moduledoc """ + Module that represent Posts published by groups + """ + use Ecto.Schema + import Ecto.Changeset + alias Ecto.Changeset + alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Tag + alias Mobilizon.Media.Picture + alias Mobilizon.Posts.Post.TitleSlug + alias Mobilizon.Posts.PostVisibility + alias Mobilizon.Web.Endpoint + alias Mobilizon.Web.Router.Helpers, as: Routes + + @type t :: %__MODULE__{ + url: String.t(), + local: boolean, + slug: String.t(), + body: String.t(), + title: String.t(), + draft: boolean, + visibility: PostVisibility.t(), + publish_at: DateTime.t(), + author: Actor.t(), + attributed_to: Actor.t(), + picture: Picture.t(), + tags: [Tag.t()] + } + + @primary_key {:id, Ecto.UUID, autogenerate: true} + + schema "posts" do + field(:body, :string) + field(:draft, :boolean, default: false) + field(:local, :boolean, default: true) + field(:slug, TitleSlug.Type) + field(:title, :string) + field(:url, :string) + field(:publish_at, :utc_datetime) + field(:visibility, PostVisibility, default_value: :public) + belongs_to(:author, Actor) + belongs_to(:attributed_to, Actor) + belongs_to(:picture, Picture, on_replace: :update) + many_to_many(:tags, Tag, join_through: "posts_tags", on_replace: :delete) + + timestamps() + end + + @required_attrs [ + :id, + :title, + :body, + :draft, + :slug, + :url, + :author_id, + :attributed_to_id + ] + @optional_attrs [:picture_id, :local, :publish_at, :visibility] + @attrs @required_attrs ++ @optional_attrs + + @doc false + def changeset(%__MODULE__{} = post, attrs) do + post + |> cast(attrs, @attrs) + |> maybe_generate_id() + |> put_tags(attrs) + |> maybe_put_publish_date() + # Validate ID and title here because they're needed for slug + |> validate_required([:id, :title]) + |> TitleSlug.maybe_generate_slug() + |> TitleSlug.unique_constraint() + |> maybe_generate_url() + |> validate_required(@required_attrs) + end + + defp maybe_generate_id(%Ecto.Changeset{} = changeset) do + case fetch_field(changeset, :id) do + res when res in [:error, {:data, nil}] -> + put_change(changeset, :id, Ecto.UUID.generate()) + + _ -> + changeset + end + end + + @spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() + defp maybe_generate_url(%Ecto.Changeset{} = changeset) do + with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url), + {changes, id_and_slug} when changes in [:changes, :data] <- + fetch_field(changeset, :slug), + url <- generate_url(id_and_slug) do + put_change(changeset, :url, url) + else + _ -> changeset + end + end + + @spec generate_url(String.t()) :: String.t() + defp generate_url(id_and_slug), do: Routes.page_url(Endpoint, :post, id_and_slug) + + @spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t() + defp put_tags(changeset, %{"tags" => tags}), + do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1)) + + defp put_tags(changeset, %{tags: tags}), + do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1)) + + defp put_tags(changeset, _), do: changeset + + defp process_tag(tag), do: Tag.changeset(%Tag{}, tag) + + defp maybe_put_publish_date(%Changeset{} = changeset) do + publish_at = + if get_field(changeset, :draft, true) == false, + do: DateTime.utc_now() |> DateTime.truncate(:second), + else: nil + + put_change(changeset, :publish_at, publish_at) + end +end diff --git a/lib/mobilizon/posts/posts.ex b/lib/mobilizon/posts/posts.ex new file mode 100644 index 000000000..b30fae8be --- /dev/null +++ b/lib/mobilizon/posts/posts.ex @@ -0,0 +1,135 @@ +defmodule Mobilizon.Posts do + @moduledoc """ + The Posts context. + """ + alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Tag + alias Mobilizon.Posts.Post + alias Mobilizon.Storage.{Page, Repo} + + import Ecto.Query + require Logger + + @post_preloads [:author, :attributed_to, :picture] + + import EctoEnum + + defenum(PostVisibility, :post_visibility, [ + :public, + :unlisted, + :restricted, + :private + ]) + + @doc """ + Returns the list of recent posts for a group + """ + @spec get_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t() + def get_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do + group_id + |> do_get_posts_for_group() + |> Page.build_page(page, limit) + end + + @spec get_public_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t() + def get_public_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do + group_id + |> do_get_posts_for_group() + |> where([p], p.visibility == ^:public and not p.draft) + |> Page.build_page(page, limit) + end + + def do_get_posts_for_group(group_id) do + Post + |> where(attributed_to_id: ^group_id) + |> order_by(desc: :inserted_at) + |> preload([p], [:author, :attributed_to, :picture]) + end + + @doc """ + Get a post by it's ID + """ + @spec get_post(integer | String.t()) :: Post.t() | nil + def get_post(nil), do: nil + def get_post(id), do: Repo.get(Post, id) + + @spec get_post_with_preloads(integer | String.t()) :: Post.t() | nil + def get_post_with_preloads(id) do + Post + |> Repo.get(id) + |> Repo.preload(@post_preloads) + end + + @spec get_post_by_slug(String.t()) :: Post.t() | nil + def get_post_by_slug(nil), do: nil + def get_post_by_slug(slug), do: Repo.get_by(Post, slug: slug) + + @spec get_post_by_slug_with_preloads(String.t()) :: Post.t() | nil + def get_post_by_slug_with_preloads(slug) do + Post + |> Repo.get_by(slug: slug) + |> Repo.preload(@post_preloads) + end + + @doc """ + Get a post by it's URL + """ + @spec get_post_by_url(String.t()) :: Post.t() | nil + def get_post_by_url(url), do: Repo.get_by(Post, url: url) + + @spec get_post_by_url_with_preloads(String.t()) :: Post.t() | nil + def get_post_by_url_with_preloads(url) do + Post + |> Repo.get_by(url: url) + |> Repo.preload(@post_preloads) + end + + @doc """ + Creates a post. + """ + @spec create_post(map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()} + def create_post(attrs \\ %{}) do + %Post{} + |> Post.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a post. + """ + @spec update_post(Post.t(), map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()} + def update_post(%Post{} = post, attrs) do + post + |> Repo.preload(:tags) + |> Post.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a post + """ + @spec delete_post(Post.t()) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()} + def delete_post(%Post{} = post), do: Repo.delete(post) + + @doc """ + Returns the list of tags for the post. + """ + @spec list_tags_for_post(integer | String.t()) :: [Tag.t()] + def list_tags_for_post(post_id) do + {:ok, uuid} = Ecto.UUID.dump(post_id) + + uuid + |> tags_for_post_query() + |> Repo.all() + end + + @spec tags_for_post_query(integer) :: Ecto.Query.t() + defp tags_for_post_query(post_id) do + from( + t in Tag, + join: p in "posts_tags", + on: t.id == p.tag_id, + where: p.post_id == ^post_id + ) + end +end diff --git a/lib/mobilizon/reports/report.ex b/lib/mobilizon/reports/report.ex index 745616823..20e556a6b 100644 --- a/lib/mobilizon/reports/report.ex +++ b/lib/mobilizon/reports/report.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.Reports.Report do import Ecto.Changeset alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Reports.{Note, ReportStatus} diff --git a/lib/mobilizon/resources/resources.ex b/lib/mobilizon/resources/resources.ex index 5d7f24bcb..8f6b2a6cb 100644 --- a/lib/mobilizon/resources/resources.ex +++ b/lib/mobilizon/resources/resources.ex @@ -23,6 +23,7 @@ defmodule Mobilizon.Resources do Resource |> where(actor_id: ^group_id) |> order_by(desc: :updated_at) + |> preload([r], [:actor, :creator]) |> Page.build_page(page, limit) end @@ -55,6 +56,7 @@ defmodule Mobilizon.Resources do Resource |> where([r], r.parent_id == ^resource_id) |> order_by(asc: :type) + |> preload([r], [:actor, :creator]) |> Page.build_page(page, limit) end diff --git a/lib/mobilizon/todos/todos.ex b/lib/mobilizon/todos/todos.ex index e903c5a3e..8910581bc 100644 --- a/lib/mobilizon/todos/todos.ex +++ b/lib/mobilizon/todos/todos.ex @@ -27,6 +27,7 @@ defmodule Mobilizon.Todos do TodoList |> where(actor_id: ^group_id) |> order_by(desc: :updated_at) + |> preload([:actor]) |> Page.build_page(page, limit) end diff --git a/lib/mobilizon/users/setting.ex b/lib/mobilizon/users/setting.ex index 1534ce214..a7fb1804c 100644 --- a/lib/mobilizon/users/setting.ex +++ b/lib/mobilizon/users/setting.ex @@ -7,6 +7,15 @@ defmodule Mobilizon.Users.Setting do import Ecto.Changeset alias Mobilizon.Users.{NotificationPendingNotificationDelay, User} + @type t :: %__MODULE__{ + timezone: String.t(), + notification_on_day: boolean, + notification_each_week: boolean, + notification_before_event: boolean, + notification_pending_participation: NotificationPendingNotificationDelay.t(), + user: User.t() + } + @required_attrs [:user_id] @optional_attrs [ diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex index 6d0f8d328..ec077d141 100644 --- a/lib/service/export/feed.ex +++ b/lib/service/export/feed.ex @@ -47,7 +47,7 @@ defmodule Mobilizon.Service.Export.Feed do defp fetch_actor_event_feed(name) do with %Actor{} = actor <- Actors.get_local_actor_by_name(name), {:visibility, true} <- {:visibility, Actor.is_public_visibility(actor)}, - {:ok, events, _count} <- Events.list_public_events_for_actor(actor) do + %Page{elements: events} <- Events.list_public_events_for_actor(actor) do {:ok, build_actor_feed(actor, events)} else err -> diff --git a/lib/service/export/icalendar.ex b/lib/service/export/icalendar.ex index b4ed5c17b..f4b0d047a 100644 --- a/lib/service/export/icalendar.ex +++ b/lib/service/export/icalendar.ex @@ -49,7 +49,8 @@ defmodule Mobilizon.Service.Export.ICalendar do @spec export_public_actor(Actor.t()) :: String.t() def export_public_actor(%Actor{} = actor) do with true <- Actor.is_public_visibility(actor), - {:ok, events, _} <- Events.list_public_events_for_actor(actor) do + %Page{elements: events} <- + Events.list_public_events_for_actor(actor) do {:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()} end end diff --git a/lib/service/geospatial/addok.ex b/lib/service/geospatial/addok.ex index 199652cf3..c31b68bcd 100644 --- a/lib/service/geospatial/addok.ex +++ b/lib/service/geospatial/addok.ex @@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Addok do """ alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Provider + alias Mobilizon.Service.HTTP.BaseClient require Logger @@ -15,26 +15,18 @@ defmodule Mobilizon.Service.Geospatial.Addok do @default_country Application.get_env(:mobilizon, __MODULE__) |> get_in([:default_country]) || "France" - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - @impl Provider @doc """ Addok implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. """ @spec geocode(String.t(), keyword()) :: list(Address.t()) def geocode(lon, lat, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:geocode, %{lon: lon, lat: lat}, options) Logger.debug("Asking addok for addresses with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"features" => features} <- body do process_data(features) else _ -> [] @@ -47,14 +39,11 @@ defmodule Mobilizon.Service.Geospatial.Addok do """ @spec search(String.t(), keyword()) :: list(Address.t()) def search(q, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:search, %{q: q}, options) Logger.debug("Asking addok for addresses with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"features" => features} <- body do process_data(features) else _ -> [] diff --git a/lib/service/geospatial/google_maps.ex b/lib/service/geospatial/google_maps.ex index 16abce2a0..4377a07f1 100644 --- a/lib/service/geospatial/google_maps.ex +++ b/lib/service/geospatial/google_maps.ex @@ -7,6 +7,7 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do alias Mobilizon.Addresses.Address alias Mobilizon.Service.Geospatial.Provider + alias Mobilizon.Service.HTTP.BaseClient require Logger @@ -28,11 +29,6 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do @api_key_missing_message "API Key required to use Google Maps" - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - @impl Provider @doc """ Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. @@ -43,12 +39,11 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do Logger.debug("Asking Google Maps for reverse geocode with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, [], @http_options), - {:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"results" => results, "status" => "OK"} <- body do Enum.map(results, fn entry -> process_data(entry, options) end) else - {:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} -> + %{"status" => "REQUEST_DENIED", "error_message" => error_message} -> raise ArgumentError, message: to_string(error_message) end end @@ -63,15 +58,14 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do Logger.debug("Asking Google Maps for addresses with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, [], @http_options), - {:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"results" => results, "status" => "OK"} <- body do results |> Enum.map(fn entry -> process_data(entry, options) end) else - {:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} -> + %{"status" => "REQUEST_DENIED", "error_message" => error_message} -> raise ArgumentError, message: to_string(error_message) - {:ok, %{"results" => [], "status" => "ZERO_RESULTS"}} -> + %{"results" => [], "status" => "ZERO_RESULTS"} -> [] end end @@ -165,18 +159,17 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do Logger.debug("Asking Google Maps for details with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, [], @http_options), - {:ok, %{"result" => %{"name" => name}, "status" => "OK"}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"result" => %{"name" => name}, "status" => "OK"} <- body do name else - {:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} -> + %{"status" => "REQUEST_DENIED", "error_message" => error_message} -> raise ArgumentError, message: to_string(error_message) - {:ok, %{"status" => "INVALID_REQUEST"}} -> + %{"status" => "INVALID_REQUEST"} -> raise ArgumentError, message: "Invalid Request" - {:ok, %{"results" => [], "status" => "ZERO_RESULTS"}} -> + %{"results" => [], "status" => "ZERO_RESULTS"} -> nil end end diff --git a/lib/service/geospatial/map_quest.ex b/lib/service/geospatial/map_quest.ex index edb597541..2e2846658 100644 --- a/lib/service/geospatial/map_quest.ex +++ b/lib/service/geospatial/map_quest.ex @@ -10,8 +10,8 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do """ alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Provider + alias Mobilizon.Service.HTTP.BaseClient require Logger @@ -21,11 +21,6 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do @api_key_missing_message "API Key required to use MapQuest" - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - @impl Provider @doc """ MapQuest implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. @@ -35,25 +30,21 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do api_key = Keyword.get(options, :api_key, @api_key) limit = Keyword.get(options, :limit, 10) open_data = Keyword.get(options, :open_data, true) - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] prefix = if open_data, do: "open", else: "www" if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message) - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get( + with {:ok, %{status: 200, body: body}} <- + BaseClient.get( "https://#{prefix}.mapquestapi.com/geocoding/v1/reverse?key=#{api_key}&location=#{ lat - },#{lon}&maxResults=#{limit}", - headers, - @http_options + },#{lon}&maxResults=#{limit}" ), - {:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do + %{"results" => results, "info" => %{"statuscode" => 0}} <- body do results |> Enum.map(&process_data/1) else - {:ok, %HTTPoison.Response{status_code: 403, body: err}} -> + {:ok, %{status: 403, body: err}} -> raise(ArgumentError, message: err) end end @@ -64,8 +55,6 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do """ @spec search(String.t(), keyword()) :: list(Address.t()) def search(q, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] limit = Keyword.get(options, :limit, 10) api_key = Keyword.get(options, :api_key, @api_key) @@ -82,12 +71,11 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do Logger.debug("Asking MapQuest for addresses with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"results" => results, "info" => %{"statuscode" => 0}} <- body do results |> Enum.map(&process_data/1) else - {:ok, %HTTPoison.Response{status_code: 403, body: err}} -> + {:ok, %{status: 403, body: err}} -> raise(ArgumentError, message: err) end end diff --git a/lib/service/geospatial/mimirsbrunn.ex b/lib/service/geospatial/mimirsbrunn.ex index b2c06293a..729d76fc7 100644 --- a/lib/service/geospatial/mimirsbrunn.ex +++ b/lib/service/geospatial/mimirsbrunn.ex @@ -8,8 +8,8 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do """ alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Provider + alias Mobilizon.Service.HTTP.BaseClient require Logger @@ -17,25 +17,17 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint]) - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - @impl Provider @doc """ Mimirsbrunn implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. """ @spec geocode(number(), number(), keyword()) :: list(Address.t()) def geocode(lon, lat, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:geocode, %{lon: lon, lat: lat}, options) Logger.debug("Asking Mimirsbrunn for reverse geocoding with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + {:ok, %{"features" => features}} <- Jason.decode(body) do process_data(features) else _ -> [] @@ -48,14 +40,11 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do """ @spec search(String.t(), keyword()) :: list(Address.t()) def search(q, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:search, %{q: q}, options) Logger.debug("Asking Mimirsbrunn for addresses with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + {:ok, %{"features" => features}} <- Jason.decode(body) do process_data(features) else _ -> [] diff --git a/lib/service/geospatial/nominatim.ex b/lib/service/geospatial/nominatim.ex index c53230ef6..ba1d535b9 100644 --- a/lib/service/geospatial/nominatim.ex +++ b/lib/service/geospatial/nominatim.ex @@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do """ alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Provider + alias Mobilizon.Service.HTTP.BaseClient require Logger @@ -14,25 +14,17 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint]) @api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key]) - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - @impl Provider @doc """ Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. """ @spec geocode(String.t(), keyword()) :: list(Address.t()) def geocode(lon, lat, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:geocode, %{lon: lon, lat: lat}, options) Logger.debug("Asking Nominatim for geocode with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"features" => features} <- body do features |> process_data() |> Enum.filter(& &1) else _ -> [] @@ -45,14 +37,11 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do """ @spec search(String.t(), keyword()) :: list(Address.t()) def search(q, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:search, %{q: q}, options) Logger.debug("Asking Nominatim for addresses with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"features" => features} <- body do features |> process_data() |> Enum.filter(& &1) else _ -> [] diff --git a/lib/service/geospatial/pelias.ex b/lib/service/geospatial/pelias.ex index 377ced94d..969a5e461 100644 --- a/lib/service/geospatial/pelias.ex +++ b/lib/service/geospatial/pelias.ex @@ -6,8 +6,8 @@ defmodule Mobilizon.Service.Geospatial.Pelias do """ alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Provider + alias Mobilizon.Service.HTTP.BaseClient require Logger @@ -15,25 +15,17 @@ defmodule Mobilizon.Service.Geospatial.Pelias do @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint]) - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - @impl Provider @doc """ Pelias implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. """ @spec geocode(number(), number(), keyword()) :: list(Address.t()) def geocode(lon, lat, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:geocode, %{lon: lon, lat: lat}, options) Logger.debug("Asking Pelias for reverse geocoding with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + {:ok, %{"features" => features}} <- Jason.decode(body) do process_data(features) else _ -> [] @@ -46,14 +38,11 @@ defmodule Mobilizon.Service.Geospatial.Pelias do """ @spec search(String.t(), keyword()) :: list(Address.t()) def search(q, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:search, %{q: q}, options) Logger.debug("Asking Pelias for addresses with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + {:ok, %{"features" => features}} <- Jason.decode(body) do process_data(features) else _ -> [] diff --git a/lib/service/geospatial/photon.ex b/lib/service/geospatial/photon.ex index 991944413..9790d382c 100644 --- a/lib/service/geospatial/photon.ex +++ b/lib/service/geospatial/photon.ex @@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Photon do """ alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Provider + alias Mobilizon.Service.HTTP.BaseClient require Logger @@ -13,11 +13,6 @@ defmodule Mobilizon.Service.Geospatial.Photon do @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint]) - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - @impl Provider @doc """ Photon implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. @@ -26,14 +21,11 @@ defmodule Mobilizon.Service.Geospatial.Photon do """ @spec geocode(number(), number(), keyword()) :: list(Address.t()) def geocode(lon, lat, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:geocode, %{lon: lon, lat: lat}, options) Logger.debug("Asking photon for reverse geocoding with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"features" => features} <- body do process_data(features) else _ -> [] @@ -46,14 +38,11 @@ defmodule Mobilizon.Service.Geospatial.Photon do """ @spec search(String.t(), keyword()) :: list(Address.t()) def search(q, options \\ []) do - user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) - headers = [{"User-Agent", user_agent}] url = build_url(:search, %{q: q}, options) Logger.debug("Asking photon for addresses with #{url}") - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(url, headers, @http_options), - {:ok, %{"features" => features}} <- Poison.decode(body) do + with {:ok, %{status: 200, body: body}} <- BaseClient.get(url), + %{"features" => features} <- body do process_data(features) else _ -> [] diff --git a/lib/service/geospatial/provider.ex b/lib/service/geospatial/provider.ex index a86fe2b33..20afc49ce 100644 --- a/lib/service/geospatial/provider.ex +++ b/lib/service/geospatial/provider.ex @@ -15,7 +15,6 @@ defmodule Mobilizon.Service.Geospatial.Provider do ## Shared options - * `:user_agent` User-Agent string to send to the backend. Defaults to `"Mobilizon"` or `Mobilizon.Config.instance_user_agent/0` * `:lang` Lang in which to prefer results. Used as a request parameter or through an `Accept-Language` HTTP header. Defaults to `"en"`. * `:country_code` An ISO 3166 country code. String or `nil` diff --git a/lib/service/http/activity_pub.ex b/lib/service/http/activity_pub.ex new file mode 100644 index 000000000..0d0f6483f --- /dev/null +++ b/lib/service/http/activity_pub.ex @@ -0,0 +1,38 @@ +defmodule Mobilizon.Service.HTTP.ActivityPub do + @moduledoc """ + Tesla HTTP Client that is preconfigured to get and post ActivityPub content + """ + + alias Mobilizon.Config + + @adapter Application.get_env(:tesla, __MODULE__, [])[:adapter] || Tesla.Adapter.Hackney + @default_opts [ + recv_timeout: 20_000 + ] + @user_agent Config.instance_user_agent() + + def client(options \\ []) do + headers = Keyword.get(options, :headers, []) + opts = Keyword.merge(@default_opts, Keyword.get(options, :opts, [])) + + middleware = [ + {Tesla.Middleware.Headers, + [{"User-Agent", @user_agent}, {"Accept", "application/activity+json"}] ++ headers}, + Tesla.Middleware.FollowRedirects, + {Tesla.Middleware.Timeout, timeout: 10_000}, + {Tesla.Middleware.JSON, decode_content_types: "application/activity+json"} + ] + + adapter = {@adapter, opts} + + Tesla.client(middleware, adapter) + end + + def get(client, url) do + Tesla.get(client, url) + end + + def post(client, url, data) do + Tesla.post(client, url, data) + end +end diff --git a/lib/service/http/base_client.ex b/lib/service/http/base_client.ex new file mode 100644 index 000000000..689ce9b97 --- /dev/null +++ b/lib/service/http/base_client.ex @@ -0,0 +1,30 @@ +defmodule Mobilizon.Service.HTTP.BaseClient do + @moduledoc """ + Tesla HTTP Basic Client + """ + + use Tesla + alias Mobilizon.Config + + @default_opts [ + recv_timeout: 20_000 + ] + + adapter(Tesla.Adapter.Hackney, @default_opts) + + @user_agent Config.instance_user_agent() + + plug(Tesla.Middleware.FollowRedirects) + + plug(Tesla.Middleware.Timeout, timeout: 10_000) + + plug(Tesla.Middleware.Headers, [{"User-Agent", @user_agent}]) + + def get(url) do + get(url) + end + + def post(url, data) do + post(url, data) + end +end diff --git a/lib/service/metadata/comment.ex b/lib/service/metadata/comment.ex index fb2df5e57..99c893642 100644 --- a/lib/service/metadata/comment.ex +++ b/lib/service/metadata/comment.ex @@ -1,6 +1,6 @@ -defimpl Mobilizon.Service.Metadata, for: Mobilizon.Conversations.Comment do +defimpl Mobilizon.Service.Metadata, for: Mobilizon.Discussions.Comment do alias Phoenix.HTML.Tag - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment def build_tags(%Comment{} = comment, _locale \\ "en") do [ diff --git a/lib/service/metadata/event.ex b/lib/service/metadata/event.ex index 4781ab277..4c4d2504d 100644 --- a/lib/service/metadata/event.ex +++ b/lib/service/metadata/event.ex @@ -2,10 +2,9 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do alias Phoenix.HTML alias Phoenix.HTML.Tag alias Mobilizon.Events.Event - alias Mobilizon.Service.Formatter.HTML, as: HTMLFormatter alias Mobilizon.Web.JsonLD.ObjectView alias Mobilizon.Web.MediaProxy - import Mobilizon.Web.Gettext + import Mobilizon.Service.Metadata.Utils, only: [process_description: 2, strip_tags: 1] def build_tags(%Event{} = event, locale \\ "en") do event = Map.put(event, :description, process_description(event.description, locale)) @@ -41,24 +40,10 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do ] end - defp process_description(nil, locale), do: process_description("", locale) - - defp process_description("", locale) do - Gettext.put_locale(locale) - gettext("The event organizer didn't add any description.") - end - - defp process_description(description, _locale) do - description - |> HTMLFormatter.strip_tags() - |> String.slice(0..200) - |> (&"#{&1}…").() - end - # Insert JSON-LD schema by hand because Tag.content_tag wants to escape it defp json(%Event{title: title} = event) do "event.json" - |> ObjectView.render(%{event: %{event | title: HTMLFormatter.strip_tags(title)}}) + |> ObjectView.render(%{event: %{event | title: strip_tags(title)}}) |> Jason.encode!() end end diff --git a/lib/service/metadata/post.ex b/lib/service/metadata/post.ex new file mode 100644 index 000000000..6553b78f9 --- /dev/null +++ b/lib/service/metadata/post.ex @@ -0,0 +1,34 @@ +defimpl Mobilizon.Service.Metadata, for: Mobilizon.Posts.Post do + alias Phoenix.HTML + alias Phoenix.HTML.Tag + alias Mobilizon.Posts.Post + alias Mobilizon.Web.JsonLD.ObjectView + import Mobilizon.Service.Metadata.Utils, only: [process_description: 2, strip_tags: 1] + + def build_tags(%Post{} = post, locale \\ "en") do + post = Map.put(post, :body, process_description(post.body, locale)) + + tags = [ + Tag.tag(:meta, property: "og:title", content: post.title), + Tag.tag(:meta, property: "og:url", content: post.url), + Tag.tag(:meta, property: "og:description", content: post.body), + Tag.tag(:meta, property: "og:type", content: "article"), + Tag.tag(:meta, property: "twitter:card", content: "summary"), + # Tell Search Engines what's the origin + Tag.tag(:link, rel: "canonical", href: post.url) + ] + + tags ++ + [ + Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"), + ~s{} |> HTML.raw() + ] + end + + # Insert JSON-LD schema by hand because Tag.content_tag wants to escape it + defp json(%Post{title: title} = post) do + "post.json" + |> ObjectView.render(%{post: %{post | title: strip_tags(title)}}) + |> Jason.encode!() + end +end diff --git a/lib/service/metadata/utils.ex b/lib/service/metadata/utils.ex index 027a78339..f2c25bdf4 100644 --- a/lib/service/metadata/utils.ex +++ b/lib/service/metadata/utils.ex @@ -3,10 +3,34 @@ defmodule Mobilizon.Service.Metadata.Utils do Tools to convert tags to string. """ + alias Mobilizon.Service.Formatter.HTML, as: HTMLFormatter alias Phoenix.HTML + import Mobilizon.Web.Gettext + @slice_limit 200 + + @spec stringify_tags(Enum.t()) :: String.t() def stringify_tags(tags), do: Enum.reduce(tags, "", &stringify_tag/2) defp stringify_tag(tag, acc) when is_tuple(tag), do: acc <> HTML.safe_to_string(tag) defp stringify_tag(tag, acc) when is_binary(tag), do: acc <> tag + + @spec strip_tags(String.t()) :: String.t() + def strip_tags(text), do: HTMLFormatter.strip_tags(text) + + @spec process_description(String.t(), String.t(), integer()) :: String.t() + def process_description(description, locale \\ "en", limit \\ @slice_limit) + def process_description(nil, locale, limit), do: process_description("", locale, limit) + + def process_description("", locale, _limit) do + Gettext.put_locale(locale) + gettext("The event organizer didn't add any description.") + end + + def process_description(description, _locale, limit) do + description + |> HTMLFormatter.strip_tags() + |> String.slice(0..limit) + |> (&"#{&1}…").() + end end diff --git a/lib/service/rich_media/favicon.ex b/lib/service/rich_media/favicon.ex index 756501bf2..12768b2de 100644 --- a/lib/service/rich_media/favicon.ex +++ b/lib/service/rich_media/favicon.ex @@ -16,24 +16,24 @@ defmodule Mobilizon.Service.RichMedia.Favicon do ssl: [{:versions, [:"tlsv1.2"]}] ] - @spec fetch(String.t(), List.t()) :: {:ok, String.t()} | {:error, any()} + @spec fetch(String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()} def fetch(url, options \\ []) do user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) headers = [{"User-Agent", user_agent}] - case HTTPoison.get(url, headers, @options) do - {:ok, %HTTPoison.Response{status_code: code, body: body}} when code in 200..299 -> + case Tesla.get(url, headers: headers, opts: @options) do + {:ok, %{status: code, body: body}} when code in 200..299 -> find_favicon_url(url, body, headers) - {:ok, %HTTPoison.Response{}} -> + {:ok, %{}} -> {:error, "Error while fetching the page"} - {:error, %HTTPoison.Error{reason: reason}} -> - {:error, reason} + {:error, err} -> + {:error, err} end end - @spec find_favicon_url(String.t(), String.t(), List.t()) :: {:ok, String.t()} | {:error, any()} + @spec find_favicon_url(String.t(), String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()} defp find_favicon_url(url, body, headers) do Logger.debug("finding favicon URL for #{url}") @@ -85,20 +85,20 @@ defmodule Mobilizon.Service.RichMedia.Favicon do end end - @spec find_favicon_in_root(String.t(), List.t()) :: {:ok, String.t()} | {:error, any()} + @spec find_favicon_in_root(String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()} defp find_favicon_in_root(url, headers) do uri = URI.parse(url) favicon_url = "#{uri.scheme}://#{uri.host}/favicon.ico" - case HTTPoison.head(favicon_url, headers, @options) do - {:ok, %HTTPoison.Response{status_code: code}} when code in 200..299 -> + case Tesla.head(favicon_url, headers: headers, opts: @options) do + {:ok, %{status: code}} when code in 200..299 -> {:ok, favicon_url} - {:ok, %HTTPoison.Response{}} -> + {:ok, %{}} -> {:error, "Error while doing a HEAD request on the favicon"} - {:error, %HTTPoison.Error{reason: reason}} -> - {:error, reason} + {:error, err} -> + {:error, err} end end end diff --git a/lib/service/rich_media/parser.ex b/lib/service/rich_media/parser.ex index 05ba6429a..fa16a328c 100644 --- a/lib/service/rich_media/parser.ex +++ b/lib/service/rich_media/parser.ex @@ -12,7 +12,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do timeout: 10_000, recv_timeout: 20_000, follow_redirect: true, - # TODO: Remove me once Hackney/HTTPoison fixes their shit with TLS1.3 and OTP 23 + # TODO: Remove me once Hackney/HTTPoison fixes their issue with TLS1.3 and OTP 23 ssl: [{:versions, [:"tlsv1.2"]}] ] @@ -46,7 +46,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do {:error, "Cachex error: #{inspect(e)}"} end - @spec parse_url(String.t(), List.t()) :: {:ok, map()} | {:error, any()} + @spec parse_url(String.t(), Enum.t()) :: {:ok, map()} | {:error, any()} defp parse_url(url, options \\ []) do user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent()) headers = [{"User-Agent", user_agent}] @@ -54,12 +54,12 @@ defmodule Mobilizon.Service.RichMedia.Parser do try do with {:ok, _} <- prevent_local_address(url), - {:ok, %HTTPoison.Response{body: body, status_code: code, headers: response_headers}} + {:ok, %{body: body, status: code, headers: response_headers}} when code in 200..299 <- - HTTPoison.get( + Tesla.get( url, - headers, - @options + headers: headers, + opts: @options ), {:is_html, _response_headers, true} <- {:is_html, response_headers, is_html(response_headers)} do @@ -87,7 +87,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do end end - @spec get_data_for_media(List.t(), String.t()) :: map() + @spec get_data_for_media(Enum.t(), String.t()) :: map() defp get_data_for_media(response_headers, url) do data = %{title: get_filename_from_headers(response_headers) || get_filename_from_url(url)} @@ -98,21 +98,21 @@ defmodule Mobilizon.Service.RichMedia.Parser do end end - @spec is_html(List.t()) :: boolean - defp is_html(headers) do + @spec is_html(Enum.t()) :: boolean + def is_html(headers) do headers |> get_header("Content-Type") |> content_type_header_matches(["text/html", "application/xhtml"]) end - @spec is_image(List.t()) :: boolean + @spec is_image(Enum.t()) :: boolean defp is_image(headers) do headers |> get_header("Content-Type") |> content_type_header_matches(["image/"]) end - @spec content_type_header_matches(String.t() | nil, List.t()) :: boolean + @spec content_type_header_matches(String.t() | nil, Enum.t()) :: boolean defp content_type_header_matches(header, content_types) defp content_type_header_matches(nil, _content_types), do: false @@ -120,15 +120,17 @@ defmodule Mobilizon.Service.RichMedia.Parser do Enum.any?(content_types, fn content_type -> String.starts_with?(header, content_type) end) end - @spec get_header(List.t(), String.t()) :: String.t() | nil + @spec get_header(Enum.t(), String.t()) :: String.t() | nil defp get_header(headers, key) do + key = String.downcase(key) + case List.keyfind(headers, key, 0) do {^key, value} -> String.downcase(value) nil -> nil end end - @spec get_filename_from_headers(List.t()) :: String.t() | nil + @spec get_filename_from_headers(Enum.t()) :: String.t() | nil defp get_filename_from_headers(headers) do case get_header(headers, "Content-Disposition") do nil -> nil @@ -138,12 +140,16 @@ defmodule Mobilizon.Service.RichMedia.Parser do @spec get_filename_from_url(String.t()) :: String.t() defp get_filename_from_url(url) do - %URI{path: path} = URI.parse(url) + case URI.parse(url) do + %URI{path: nil} -> + nil - path - |> String.split("/", trim: true) - |> Enum.at(-1) - |> URI.decode() + %URI{path: path} -> + path + |> String.split("/", trim: true) + |> Enum.at(-1) + |> URI.decode() + end end # The following is taken from https://github.com/elixir-plug/plug/blob/65986ad32f9aaae3be50dc80cbdd19b326578da7/lib/plug/parsers/multipart.ex#L207 diff --git a/lib/service/rich_media/parsers/oembed_parser.ex b/lib/service/rich_media/parsers/oembed_parser.ex index 8f468f921..e1e3c19f8 100644 --- a/lib/service/rich_media/parsers/oembed_parser.ex +++ b/lib/service/rich_media/parsers/oembed_parser.ex @@ -42,7 +42,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do end defp get_oembed_data(url) do - with {:ok, %HTTPoison.Response{body: json}} <- HTTPoison.get(url, [], @http_options), + with {:ok, %{body: json}} <- Tesla.get(url, opts: @http_options), {:ok, data} <- Jason.decode(json), data <- data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) do {:ok, data} diff --git a/lib/service/rich_media/parsers/ogp.ex b/lib/service/rich_media/parsers/ogp.ex index 0b879aab1..7bfcf88ed 100644 --- a/lib/service/rich_media/parsers/ogp.ex +++ b/lib/service/rich_media/parsers/ogp.ex @@ -34,7 +34,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OGP do |> Map.put(:height, get_integer_value(data, :"image:height")) end - @spec get_integer_value(Map.t(), atom()) :: integer() | nil + @spec get_integer_value(map(), atom()) :: integer() | nil defp get_integer_value(data, key) do with value when not is_nil(value) <- Map.get(data, key), {value, ""} <- Integer.parse(value) do diff --git a/lib/service/statistics/statistics.ex b/lib/service/statistics/statistics.ex index e7035cc8e..105e33040 100644 --- a/lib/service/statistics/statistics.ex +++ b/lib/service/statistics/statistics.ex @@ -3,7 +3,7 @@ defmodule Mobilizon.Service.Statistics do A module that provides cached statistics """ - alias Mobilizon.{Conversations, Events, Users} + alias Mobilizon.{Discussions, Events, Users} def get_cached_value(key) do case Cachex.fetch(:statistics, key, fn key -> @@ -26,6 +26,6 @@ defmodule Mobilizon.Service.Statistics do end defp create_cache(:local_comments) do - Conversations.count_local_comments() + Discussions.count_local_comments() end end diff --git a/lib/service/workers/notification.ex b/lib/service/workers/notification.ex index 3cb0346b1..ec95fa987 100644 --- a/lib/service/workers/notification.ex +++ b/lib/service/workers/notification.ex @@ -98,7 +98,7 @@ defmodule Mobilizon.Service.Workers.Notification do else err -> require Logger - Logger.error(inspect(err)) + Logger.debug(inspect(err)) end end diff --git a/lib/web/cache/activity_pub.ex b/lib/web/cache/activity_pub.ex index 9475c30f0..a6d557902 100644 --- a/lib/web/cache/activity_pub.ex +++ b/lib/web/cache/activity_pub.ex @@ -3,11 +3,12 @@ defmodule Mobilizon.Web.Cache.ActivityPub do ActivityPub related cache. """ - alias Mobilizon.{Actors, Conversations, Events, Resources, Todos, Tombstone} + alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone} alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub.Relay + alias Mobilizon.Posts.Post alias Mobilizon.Resources.Resource alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Web.Endpoint @@ -61,7 +62,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do {:commit, Comment.t()} | {:ignore, nil} def get_comment_by_uuid_with_preload(uuid) do Cachex.fetch(@cache, "comment_" <> uuid, fn "comment_" <> uuid -> - case Conversations.get_comment_from_uuid_with_preload(uuid) do + case Discussions.get_comment_from_uuid_with_preload(uuid) do %Comment{} = comment -> {:commit, comment} @@ -88,6 +89,40 @@ defmodule Mobilizon.Web.Cache.ActivityPub do end) end + @doc """ + Gets a post by its slug, with all associations loaded. + """ + @spec get_post_by_slug_with_preload(String.t()) :: + {:commit, Post.t()} | {:ignore, nil} + def get_post_by_slug_with_preload(slug) do + Cachex.fetch(@cache, "post_" <> slug, fn "post_" <> slug -> + case Posts.get_post_by_slug_with_preloads(slug) do + %Post{} = post -> + {:commit, post} + + nil -> + {:ignore, nil} + end + end) + end + + @doc """ + Gets a discussion by its slug, with all associations loaded. + """ + @spec get_discussion_by_slug_with_preload(String.t()) :: + {:commit, Discussion.t()} | {:ignore, nil} + def get_discussion_by_slug_with_preload(slug) do + Cachex.fetch(@cache, "discussion_" <> slug, fn "discussion_" <> slug -> + case Discussions.get_discussion_by_slug(slug) do + %Discussion{} = discussion -> + {:commit, discussion} + + nil -> + {:ignore, nil} + end + end) + end + @doc """ Gets a todo list by its UUID, with all associations loaded. """ diff --git a/lib/web/cache/cache.ex b/lib/web/cache/cache.ex index eb60c7e81..31542c385 100644 --- a/lib/web/cache/cache.ex +++ b/lib/web/cache/cache.ex @@ -23,5 +23,7 @@ defmodule Mobilizon.Web.Cache do defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub + defdelegate get_post_by_slug_with_preload(slug), to: ActivityPub + defdelegate get_discussion_by_slug_with_preload(slug), to: ActivityPub defdelegate get_relay, to: ActivityPub end diff --git a/lib/web/controllers/activity_pub_controller.ex b/lib/web/controllers/activity_pub_controller.ex index 99cd5bd8d..e4739a4d9 100644 --- a/lib/web/controllers/activity_pub_controller.ex +++ b/lib/web/controllers/activity_pub_controller.ex @@ -14,6 +14,7 @@ defmodule Mobilizon.Web.ActivityPubController do alias Mobilizon.Web.ActivityPub.ActorView alias Mobilizon.Web.Cache + alias Plug.Conn require Logger @@ -33,96 +34,40 @@ defmodule Mobilizon.Web.ActivityPubController do end end - def following(conn, %{"name" => name, "page" => page}) do - with {page, ""} <- Integer.parse(page), - %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json(ActorView.render("following.json", %{actor: actor, page: page})) - end + def following(conn, args) do + actor_collection(conn, "following", args) end - def following(conn, %{"name" => name}) do - with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json(ActorView.render("following.json", %{actor: actor})) - end + def followers(conn, args) do + actor_collection(conn, "followers", args) end - def followers(conn, %{"name" => name, "page" => page}) do - with {page, ""} <- Integer.parse(page), - %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json(ActorView.render("followers.json", %{actor: actor, page: page})) - end + def members(conn, args) do + actor_collection(conn, "members", args) end - def followers(conn, %{"name" => name}) do - with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json(ActorView.render("followers.json", %{actor: actor})) - end + def resources(conn, args) do + actor_collection(conn, "resources", args) end - def members(conn, %{"name" => name, "page" => page}) do - with {page, ""} <- Integer.parse(page), - %Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json( - ActorView.render("members.json", %{ - group: group, - page: page, - actor_applicant: Map.get(conn.assigns, :actor) - }) - ) - end + def posts(conn, args) do + actor_collection(conn, "posts", args) end - def members(conn, %{"name" => name}) do - with %Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json( - ActorView.render("members.json", %{ - group: group, - actor_applicant: Map.get(conn.assigns, :actor) - }) - ) - end + def todos(conn, args) do + actor_collection(conn, "todos", args) end - def resources(conn, %{"name" => name}) do - with %Actor{} = group <- Actors.get_local_actor_by_name_with_preload(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json( - ActorView.render("resources.json", %{ - group: group, - actor_applicant: Map.get(conn.assigns, :actor) - }) - ) - end + def events(conn, args) do + actor_collection(conn, "events", args) end - def outbox(conn, %{"name" => name, "page" => page}) do - with {page, ""} <- Integer.parse(page), - %Actor{} = actor <- Actors.get_local_actor_by_name(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json(ActorView.render("outbox.json", %{actor: actor, page: page})) - end + def discussions(conn, args) do + actor_collection(conn, "discussions", args) end - def outbox(conn, %{"name" => name}) do - with %Actor{} = actor <- Actors.get_local_actor_by_name(name) do - conn - |> put_resp_header("content-type", "application/activity+json") - |> json(ActorView.render("outbox.json", %{actor: actor})) - end + def outbox(conn, args) do + actor_collection(conn, "outbox", args) end # TODO: Ensure that this inbox is a recipient of the message @@ -178,4 +123,34 @@ defmodule Mobilizon.Web.ActivityPubController do |> put_status(500) |> json("Unknown Error") end + + @spec actor_collection(Conn.t(), String.t(), map()) :: Conn.t() + + defp actor_collection(conn, collection, %{"name" => name, "page" => page}) do + with {page, ""} <- Integer.parse(page), + %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json( + ActorView.render("#{collection}.json", %{ + actor: actor, + page: page, + actor_applicant: Map.get(conn.assigns, :actor) + }) + ) + end + end + + defp actor_collection(conn, collection, %{"name" => name}) do + with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json( + ActorView.render("#{collection}.json", %{ + actor: actor, + actor_applicant: Map.get(conn.assigns, :actor) + }) + ) + end + end end diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex index 18b65b180..c57663768 100644 --- a/lib/web/controllers/page_controller.ex +++ b/lib/web/controllers/page_controller.ex @@ -4,7 +4,7 @@ defmodule Mobilizon.Web.PageController do """ use Mobilizon.Web, :controller - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub alias Mobilizon.Tombstone @@ -40,14 +40,36 @@ defmodule Mobilizon.Web.PageController do render_or_error(conn, &checks?/3, status, :resource, resource) end - def resources(conn, %{"name" => _name}) do - case get_format(conn) do - "html" -> - render(conn, :index) + @spec post(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found} + def post(conn, %{"slug" => slug}) do + {status, post} = Cache.get_post_by_slug_with_preload(slug) + render_or_error(conn, &checks?/3, status, :post, post) + end - "activity-json" -> - ActivityPubController.call(conn, :resources) - end + @spec discussion(Plug.Conn.t(), map()) :: Plug.Conn.t() | {:error, :not_found} + def discussion(conn, %{"slug" => slug}) do + {status, discussion} = Cache.get_discussion_by_slug_with_preload(slug) + render_or_error(conn, &checks?/3, status, :discussion, discussion) + end + + def resources(conn, %{"name" => _name}) do + handle_collection_route(conn, :resources) + end + + def posts(conn, %{"name" => _name}) do + handle_collection_route(conn, :posts) + end + + def discussions(conn, %{"name" => _name}) do + handle_collection_route(conn, :discussions) + end + + def events(conn, %{"name" => _name}) do + handle_collection_route(conn, :events) + end + + def todos(conn, %{"name" => _name}) do + handle_collection_route(conn, :todos) end @spec todo_list(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t() @@ -71,6 +93,16 @@ defmodule Mobilizon.Web.PageController do end end + defp handle_collection_route(conn, collection) do + case get_format(conn) do + "html" -> + render(conn, :index) + + "activity-json" -> + ActivityPubController.call(conn, collection) + end + end + defp render_or_error(conn, check_fn, status, object_type, object) do case check_fn.(conn, status, object) do true -> diff --git a/lib/web/email/event.ex b/lib/web/email/event.ex index a56a8c17b..32336bf6b 100644 --- a/lib/web/email/event.ex +++ b/lib/web/email/event.ex @@ -82,12 +82,4 @@ defmodule Mobilizon.Web.Email.Event do |> Email.Event.event_updated(actor, old_event, event, diff, locale) |> Email.Mailer.deliver_later() end - - defp send_notification_for_event_update_to_participant(user, old_event, new_event, diff) do - require Logger - Logger.error(inspect(user)) - Logger.error(inspect(old_event)) - Logger.error(inspect(new_event)) - Logger.error(inspect(diff)) - end end diff --git a/lib/web/proxy/reverse_proxy.ex b/lib/web/proxy/reverse_proxy.ex index 045e8ae23..ea1a57560 100644 --- a/lib/web/proxy/reverse_proxy.ex +++ b/lib/web/proxy/reverse_proxy.ex @@ -84,7 +84,6 @@ defmodule Mobilizon.Web.ReverseProxy do | {:redirect_on_failure, boolean} @hackney Application.get_env(:mobilizon, :hackney, :hackney) - @httpoison Application.get_env(:mobilizon, :httpoison, HTTPoison) @default_hackney_options [] @@ -108,7 +107,6 @@ defmodule Mobilizon.Web.ReverseProxy do hackney_opts = @default_hackney_options |> Keyword.merge(Keyword.get(opts, :http, [])) - |> @httpoison.process_request_options() req_headers = build_req_headers(conn.req_headers, opts) diff --git a/lib/web/router.ex b/lib/web/router.ex index 725afa2d8..15ec71b39 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -87,17 +87,23 @@ defmodule Mobilizon.Web.Router do get("/resource/:uuid", PageController, :resource, as: "resource") get("/todo-list/:uuid", PageController, :todo_list, as: "todo_list") get("/todo/:uuid", PageController, :todo, as: "todo") + get("/@:name/todos", PageController, :todos) get("/@:name/resources", PageController, :resources) + get("/@:name/posts", PageController, :posts) + get("/@:name/discussions", PageController, :discussions) + get("/@:name/events", PageController, :events) + get("/p/:slug", PageController, :post) + get("/@:name/c/:slug", PageController, :discussion) end scope "/", Mobilizon.Web do pipe_through(:activity_pub) + pipe_through(:activity_pub_signature) get("/@:name/outbox", ActivityPubController, :outbox) get("/@:name/following", ActivityPubController, :following) get("/@:name/followers", ActivityPubController, :followers) get("/@:name/members", ActivityPubController, :members) - get("/@:name/todo-lists", ActivityPubController, :todo_lists) end scope "/", Mobilizon.Web do diff --git a/lib/web/upload/upload.ex b/lib/web/upload/upload.ex index dd6ee0652..0435336b6 100644 --- a/lib/web/upload/upload.ex +++ b/lib/web/upload/upload.ex @@ -64,7 +64,7 @@ defmodule Mobilizon.Web.Upload do } defstruct [:id, :name, :tempfile, :content_type, :path, :size] - @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()} + @spec store(source, options :: [option()]) :: {:ok, map()} | {:error, any()} def store(upload, opts \\ []) do opts = get_opts(opts) diff --git a/lib/web/upload/uploader/uploader.ex b/lib/web/upload/uploader/uploader.ex index fce8fdf62..57c9de994 100644 --- a/lib/web/upload/uploader/uploader.ex +++ b/lib/web/upload/uploader/uploader.ex @@ -37,7 +37,7 @@ defmodule Mobilizon.Web.Upload.Uploader do @callback remove_file(file_spec()) :: :ok | {:ok, file_spec()} | {:error, String.t()} - @callback http_callback(Plug.Conn.t(), Map.t()) :: + @callback http_callback(Plug.Conn.t(), map()) :: {:ok, Plug.Conn.t()} | {:ok, Plug.Conn.t(), file_spec()} | {:error, Plug.Conn.t(), String.t()} diff --git a/lib/web/views/activity_pub/actor_view.ex b/lib/web/views/activity_pub/actor_view.ex index a1c33d9e6..3599cf392 100644 --- a/lib/web/views/activity_pub/actor_view.ex +++ b/lib/web/views/activity_pub/actor_view.ex @@ -1,16 +1,21 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do use Mobilizon.Web, :view - alias Mobilizon.Actors + alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos} alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Resources - alias Mobilizon.Resources.Resource - + alias Mobilizon.Discussions.Discussion + alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Activity, Utils} alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Posts.Post + alias Mobilizon.Resources.Resource + alias Mobilizon.Storage.Page + alias Mobilizon.Todos.TodoList @private_visibility_empty_collection %{elements: [], total: 0} + @json_ld_header Utils.make_json_ld_header() + @selected_member_roles ~w(creator administrator moderator member)a def render("actor.json", %{actor: actor}) do actor @@ -18,145 +23,120 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do |> Map.merge(Utils.make_json_ld_header()) end - def render("following.json", %{actor: actor, page: page}) do - %{total: total, elements: following} = - if Actor.is_public_visibility(actor), - do: Actors.build_followings_for_actor(actor, page), - else: @private_visibility_empty_collection + @doc """ + Render an actor collection + """ + @spec render(String.t(), map()) :: map() + def render(view_name, %{actor: %Actor{} = actor} = args) do + is_root? = is_nil(Map.get(args, :page)) + page = Map.get(args, :page, 1) + collection_name = String.trim_trailing(view_name, ".json") + collection_name = String.to_existing_atom(collection_name) - following - |> collection(actor.preferred_username, :following, page, total) - |> Map.merge(Utils.make_json_ld_header()) + %{total: total, elements: elements} = + if can_get_collection?(collection_name, actor, Map.get(args, :actor_applicant)), + do: fetch_collection(collection_name, actor, page), + else: default_collection(collection_name, actor, page) + + collection = + if is_root? do + root_collection(elements, actor, collection_name, total) + else + collection(elements, actor.preferred_username, collection_name, page, total) + end + + Map.merge(collection, @json_ld_header) end - def render("following.json", %{actor: actor}) do - %{total: total, elements: following} = - if Actor.is_public_visibility(actor), - do: Actors.build_followings_for_actor(actor), - else: @private_visibility_empty_collection - + @spec root_collection(Enum.t(), Actor.t(), atom(), integer()) :: map() + defp root_collection( + elements, + %Actor{preferred_username: preferred_username, url: actor_url}, + collection, + total + ) do %{ - "id" => Actor.build_url(actor.preferred_username, :following), + "id" => Actor.build_url(preferred_username, collection), + "attributedTo" => actor_url, "type" => "OrderedCollection", "totalItems" => total, - "first" => collection(following, actor.preferred_username, :following, 1, total) + "first" => collection(elements, preferred_username, collection, 1, total) } - |> Map.merge(Utils.make_json_ld_header()) end - def render("followers.json", %{actor: actor, page: page}) do - %{total: total, elements: followers} = - if Actor.is_public_visibility(actor), - do: Actors.build_followers_for_actor(actor, page), - else: @private_visibility_empty_collection - - followers - |> collection(actor.preferred_username, :followers, page, total) - |> Map.merge(Utils.make_json_ld_header()) + @spec fetch_collection(atom(), Actor.t(), integer()) :: Page.t() + defp fetch_collection(:following, actor, page) do + Actors.build_followings_for_actor(actor, page) end - def render("followers.json", %{actor: actor}) do - %{total: total, elements: followers} = - if Actor.is_public_visibility(actor), - do: Actors.build_followers_for_actor(actor), - else: @private_visibility_empty_collection - - %{ - "id" => actor.followers_url, - "type" => "OrderedCollection", - "totalItems" => total, - "first" => collection(followers, actor.preferred_username, :followers, 1, total) - } - |> Map.merge(Utils.make_json_ld_header()) + defp fetch_collection(:followers, actor, page) do + Actors.build_followers_for_actor(actor, page) end - def render("members.json", %{group: group, page: page, actor_applicant: actor_applicant}) do - %{total: total, elements: members} = - if Actor.is_public_visibility(group) || - actor_applicant_group_member?(group, actor_applicant), - do: Actors.list_members_for_group(group, page), - else: @private_visibility_empty_collection - - members - |> collection(group.preferred_username, :members, page, total) - |> Map.merge(Utils.make_json_ld_header()) + defp fetch_collection(:members, actor, page) do + Actors.list_members_for_group(actor, @selected_member_roles, page) end - def render("members.json", %{group: group, actor_applicant: actor_applicant}) do - %{total: total, elements: members} = - if Actor.is_public_visibility(group) || - actor_applicant_group_member?(group, actor_applicant), - do: Actors.list_members_for_group(group), - else: @private_visibility_empty_collection - - %{ - "id" => group.url, - "attributedTo" => group.url, - "type" => "OrderedCollection", - "totalItems" => total, - "first" => collection(members, group.preferred_username, :members, 1, total) - } - |> Map.merge(Utils.make_json_ld_header()) + defp fetch_collection(:resources, actor, page) do + Resources.get_resources_for_group(actor, page) end - def render("resources.json", %{group: group, page: page, actor_applicant: actor_applicant}) do - %{total: total, elements: resources} = - if Actor.is_public_visibility(group) || - actor_applicant_group_member?(group, actor_applicant), - do: Resources.get_top_level_resources_for_group(group), - else: @private_visibility_empty_collection - - resources - |> collection(group.preferred_username, :resources, page, total) - |> Map.merge(Utils.make_json_ld_header()) + defp fetch_collection(:discussions, actor, page) do + Discussions.find_discussions_for_actor(actor.id, page) end - def render("resources.json", %{group: group, actor_applicant: actor_applicant}) do - %{total: total, elements: resources} = - if Actor.is_public_visibility(group) || - actor_applicant_group_member?(group, actor_applicant), - do: Resources.get_top_level_resources_for_group(group), - else: @private_visibility_empty_collection - - %{ - "id" => group.resources_url, - "attributedTo" => group.url, - "type" => "OrderedCollection", - "totalItems" => total, - "first" => collection(resources, group.preferred_username, :resources, 1, total) - } - |> Map.merge(Utils.make_json_ld_header()) + defp fetch_collection(:posts, actor, page) do + Posts.get_posts_for_group(actor, page) end - def render("outbox.json", %{actor: actor, page: page}) do - %{total: total, elements: followers} = - if Actor.is_public_visibility(actor), - do: ActivityPub.fetch_public_activities_for_actor(actor, page), - else: @private_visibility_empty_collection - - followers - |> collection(actor.preferred_username, :outbox, page, total) - |> Map.merge(Utils.make_json_ld_header()) + defp fetch_collection(:events, actor, page) do + Events.list_organized_events_for_group(actor, page) end - def render("outbox.json", %{actor: actor}) do - %{total: total, elements: followers} = - if Actor.is_public_visibility(actor), - do: ActivityPub.fetch_public_activities_for_actor(actor), - else: @private_visibility_empty_collection - - %{ - "id" => Actor.build_url(actor.preferred_username, :outbox), - "type" => "OrderedCollection", - "totalItems" => total, - "first" => collection(followers, actor.preferred_username, :outbox, 1, total) - } - |> Map.merge(Utils.make_json_ld_header()) + defp fetch_collection(:todos, actor, page) do + Todos.get_todo_lists_for_group(actor, page) end + @spec fetch_collection(atom(), Actor.t(), integer()) :: %{total: integer(), elements: Enum.t()} + defp fetch_collection(:outbox, actor, page) do + ActivityPub.fetch_public_activities_for_actor(actor, page) + end + + defp fetch_collection(_, _, _), do: @private_visibility_empty_collection + + @spec can_get_collection?(atom(), Actor.t(), Actor.t()) :: boolean() + # Outbox only contains public activities + defp can_get_collection?(collection, %Actor{visibility: visibility} = _actor, _actor_applicant) + when visibility in [:public, :unlisted] and collection in [:outbox, :followers, :following], + do: true + + defp can_get_collection?(_collection_name, %Actor{} = actor, %Actor{} = actor_applicant), + do: actor_applicant_group_member?(actor, actor_applicant) + + defp can_get_collection?(_, _, _), do: false + + # Posts and events allows to browse public content + defp default_collection(:posts, %Actor{} = actor, page), + do: Posts.get_public_posts_for_group(actor, page) + + defp default_collection(:events, %Actor{} = actor, page), + do: Events.list_public_events_for_actor(actor, page) + + defp default_collection(_, _, _), do: @private_visibility_empty_collection + @spec collection(list(), String.t(), atom(), integer(), integer()) :: map() defp collection(collection, preferred_username, endpoint, page, total) - when endpoint in [:followers, :following, :outbox, :members, :resources, :todos] do + when endpoint in [ + :followers, + :following, + :outbox, + :members, + :resources, + :todos, + :posts, + :events, + :discussions + ] do offset = (page - 1) * 10 map = %{ @@ -178,6 +158,10 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do def item(%Actor{url: url}), do: url def item(%Member{} = member), do: Convertible.model_to_as(member) def item(%Resource{} = resource), do: Convertible.model_to_as(resource) + def item(%Discussion{} = discussion), do: Convertible.model_to_as(discussion) + def item(%Post{} = post), do: Convertible.model_to_as(post) + def item(%Event{} = event), do: Convertible.model_to_as(event) + def item(%TodoList{} = todo_list), do: Convertible.model_to_as(todo_list) defp actor_applicant_group_member?(%Actor{}, nil), do: false diff --git a/lib/web/views/json_ld/object_view.ex b/lib/web/views/json_ld/object_view.ex index 55aad9fe4..03fa0fbb2 100644 --- a/lib/web/views/json_ld/object_view.ex +++ b/lib/web/views/json_ld/object_view.ex @@ -4,24 +4,30 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do alias Mobilizon.Actors.Actor alias Mobilizon.Addresses.Address alias Mobilizon.Events.Event - + alias Mobilizon.Posts.Post alias Mobilizon.Web.JsonLD.ObjectView alias Mobilizon.Web.MediaProxy def render("event.json", %{event: %Event{} = event}) do - # TODO: event.description is actually markdown! + organizer = %{ + "@type" => if(event.organizer_actor.type == :Group, do: "Organization", else: "Person"), + "name" => Actor.display_name(event.organizer_actor) + } json_ld = %{ "@context" => "https://schema.org", "@type" => "Event", "name" => event.title, "description" => event.description, - "performer" => %{ - "@type" => - if(event.organizer_actor.type == :Group, do: "PerformingGroup", else: "Person"), - "name" => Actor.display_name(event.organizer_actor) - }, - "location" => render_one(event.physical_address, ObjectView, "place.json", as: :address) + # We assume for now performer == organizer + "performer" => organizer, + "organizer" => organizer, + "location" => render_one(event.physical_address, ObjectView, "place.json", as: :address), + "eventStatus" => + if(event.status == :cancelled, + do: "https://schema.org/EventCancelled", + else: "https://schema.org/EventScheduled" + ) } json_ld = @@ -62,4 +68,18 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do end def render("place.json", nil), do: %{} + + def render("post.json", %{post: %Post{} = post}) do + %{ + "@context" => "https://schema.org", + "@type" => "Article", + "name" => post.title, + "author" => %{ + "@type" => "Organization", + "name" => Actor.display_name(post.attributed_to) + }, + "datePublished" => post.publish_at, + "dateModified" => post.updated_at + } + end end diff --git a/lib/web/views/page_view.ex b/lib/web/views/page_view.ex index 3615ddc15..b70a4ff43 100644 --- a/lib/web/views/page_view.ex +++ b/lib/web/views/page_view.ex @@ -6,7 +6,7 @@ defmodule Mobilizon.Web.PageView do use Mobilizon.Web, :view alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.Event alias Mobilizon.Resources.Resource alias Mobilizon.Tombstone @@ -42,6 +42,12 @@ defmodule Mobilizon.Web.PageView do |> Map.merge(Utils.make_json_ld_header()) end + def render("discussion.activity-json", %{conn: %{assigns: %{object: %Discussion{} = resource}}}) do + resource + |> Convertible.model_to_as() + |> Map.merge(Utils.make_json_ld_header()) + end + def render("resource.activity-json", %{conn: %{assigns: %{object: %Resource{} = resource}}}) do resource |> Convertible.model_to_as() @@ -49,12 +55,15 @@ defmodule Mobilizon.Web.PageView do end def render(page, %{object: object, conn: conn} = _assigns) - when page in ["actor.html", "event.html", "comment.html"] do + when page in ["actor.html", "event.html", "comment.html", "post.html"] do locale = get_locale(conn) tags = object |> Metadata.build_tags(locale) inject_tags(tags, locale) end + # Discussions are private, no need to embed metadata + def render("discussion.html", params), do: render("index.html", params) + def render("index.html", %{conn: conn}) do tags = Instance.build_tags() inject_tags(tags, get_locale(conn)) diff --git a/lib/web/views/utils.ex b/lib/web/views/utils.ex index 234e222cf..5fd5417d6 100644 --- a/lib/web/views/utils.ex +++ b/lib/web/views/utils.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.Web.Views.Utils do alias Mobilizon.Service.Metadata.Utils, as: MetadataUtils - @spec inject_tags(List.t(), String.t()) :: {:safe, String.t()} + @spec inject_tags(Enum.t(), String.t()) :: {:safe, String.t()} def inject_tags(tags, locale \\ "en") do with {:ok, index_content} <- File.read(index_file_path()) do do_replacements(index_content, MetadataUtils.stringify_tags(tags), locale) diff --git a/mix.exs b/mix.exs index 1fb048c12..6434273ac 100644 --- a/mix.exs +++ b/mix.exs @@ -87,7 +87,6 @@ defmodule Mobilizon.Mixfile do {:timex, "~> 3.0"}, {:icalendar, github: "tcitworld/icalendar"}, {:exgravatar, "~> 2.0.1"}, - {:httpoison, "~> 1.0"}, # {:json_ld, "~> 0.3"}, {:jason, "~> 1.2"}, {:ex_crypto, "~> 0.10.0"}, @@ -118,7 +117,7 @@ defmodule Mobilizon.Mixfile do {:ex_optimizer, "~> 0.1"}, {:progress_bar, "~> 2.0"}, {:oban, "~> 1.2.0"}, - {:floki, "~> 0.26.0"}, + {:floki, "~> 0.27.0"}, {:ip_reserved, "~> 0.1.0"}, {:fast_sanitize, "~> 0.1"}, {:ueberauth, "~> 0.6"}, @@ -131,6 +130,8 @@ defmodule Mobilizon.Mixfile do git: "https://github.com/tcitworld/ueberauth_keycloak.git", branch: "upgrade-deps"}, {:ueberauth_gitlab_strategy, git: "https://github.com/tcitworld/ueberauth_gitlab.git", branch: "upgrade-deps"}, + {:ecto_shortuuid, "~> 0.1"}, + {:tesla, "~> 1.3.0"}, # Dev and test dependencies {:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]}, {:ex_machina, "~> 2.3", only: [:dev, :test]}, @@ -142,7 +143,8 @@ defmodule Mobilizon.Mixfile do {:exvcr, "~> 0.10", only: :test}, {:credo, "~> 1.4.0", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.4", only: :test}, - {:elixir_feed_parser, "~> 2.1.0", only: :test} + {:elixir_feed_parser, "~> 2.1.0", only: :test}, + {:mox, "~> 0.5", only: :test} ] ++ oauth_deps() end @@ -335,7 +337,7 @@ defmodule Mobilizon.Mixfile do Mobilizon.GraphQL.Schema.Actors.PersonType, Mobilizon.GraphQL.Schema.AddressType, Mobilizon.GraphQL.Schema.AdminType, - Mobilizon.GraphQL.Schema.Conversations.CommentType, + Mobilizon.GraphQL.Schema.Discussions.CommentType, Mobilizon.GraphQL.Schema.ConfigType, Mobilizon.GraphQL.Schema.EventType, Mobilizon.GraphQL.Schema.Events.FeedTokenType, diff --git a/mix.lock b/mix.lock index 9445c84ab..e4521e3ec 100644 --- a/mix.lock +++ b/mix.lock @@ -23,12 +23,13 @@ "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, - "earmark": {:hex, :earmark, "1.4.9", "837e4c1c5302b3135e9955f2bbf52c6c52e950c383983942b68b03909356c0d9", [:mix], [{:earmark_parser, ">= 1.4.9", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "0d72df7d13a3dc8422882bed5263fdec5a773f56f7baeb02379361cb9e5b0d8e"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.9", "819bda2049e6ee1365424e4ced1ba65806eacf0d2867415f19f3f80047f8037b", [:mix], [], "hexpm", "8bf54fddabf2d7e137a0c22660e71b49d5a0a82d1fb05b5af62f2761cd6485c4"}, + "earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, "ecto_autoslug_field": {:hex, :ecto_autoslug_field, "2.0.1", "2177c1c253f6dd3efd4b56d1cb76104d0a6ef044c6b9a7a0ad6d32665c4111e5", [:mix], [{:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:slugger, ">= 0.2.0", [hex: :slugger, repo: "hexpm", optional: false]}], "hexpm", "a3cc73211f2e75b89a03332183812ebe1ac08be2e25a1df5aa3d1422f92c45c3"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, - "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"}, + "ecto_shortuuid": {:hex, :ecto_shortuuid, "0.1.3", "d36aede64edf256e4b769be2ad15a8ad5d9d1ff8ad46befe39e8cb4489abcd05", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:shortuuid, "~> 2.1.1", [hex: :shortuuid, repo: "hexpm", optional: false]}], "hexpm", "d215c8ced7125265de94d55abc696125942caef33439cf281fafded9744a4294"}, + "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, "elixir_feed_parser": {:hex, :elixir_feed_parser, "2.1.0", "bb96fb6422158dc7ad59de62ef211cc69d264acbbe63941a64a5dce97bbbc2e6", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2d3c62fe7b396ee3b73d7160bc8fadbd78bfe9597c98c7d79b3f1038d9cba28f"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, @@ -36,25 +37,25 @@ "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, "ex_cldr": {:hex, :ex_cldr, "2.16.1", "905b03c38b5fb51668a347f2e6b586bcb2c0816cd98f7d913104872c43cbc61f", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:cldr_utils, "~> 2.9", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "006e500769982e57e6f3e32cbc4664345f78b014bb5ff48ddc394d67c86c1a8d"}, "ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.9.0", "ace1c57ba3850753c9ac6ddb89dc0c9a9e5e1c57ecad587e21c8925ad30a3838", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.13", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.0", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a4b07773e2a326474f44a6bc51fffbec634859a1bad5cc6e6eb55eba45115541"}, - "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.5.0", "e369ae3c1cd5cd20aa20988b153fd2902b4ab08aec63ca8757d7104bdb79f867", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ba16b1df60bcec52c986481bbdfa7cfaec899b610f869d2b3c5a9a8149f67668"}, + "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.6.1", "accc6b409c88c3ce4d9439f0a92e375470b788efa95d8fa5a054aa9e8950e972", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ca9817eadc78f11f1084f64c579288513b0042eaa39ea2337d68ba959ebae1ad"}, "ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.5.1", "9439d1c40cfd03c3d8f3f60f5d3e3f2c6eaf0fd714541d687531cce78cfb9909", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.8", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.15", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "62a2f8d41ec6e789137bbf3ac7c944885a8ef6b7ce475905d056d1805b482427"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.1", "dced7ffee69c4830593258b69b294adb4c65cf539e1d8ae0a4de31cfc8aa56a0", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.5", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "c6a4b69ef80b8ffbb6c8fb69a2b365ba542580e0f76a15d8c6ee9142bd1b97ea"}, "ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"}, - "ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"}, + "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, "ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "db76473b2ae0259e6633c6c479a5a4d8603f09497f55c88f9ef4d53d2b75befb"}, "ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"}, "ex_optimizer": {:hex, :ex_optimizer, "0.1.0", "1d12f7ea289092a38a794b84bd2f42c1e0621cb307c0f3e6a7df620839af2937", [:mix], [{:file_info, "~> 0.0.4", [hex: :file_info, repo: "hexpm", optional: false]}], "hexpm", "a409cb91472e08d4791a129effe4687982f85e2debcb4ccb1a3711a36bfdc428"}, "ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm", "fddf5054dd5fd2f809e837b749570baa5c9798e11d0163921baec49b7d5762f2"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, - "excoveralls": {:hex, :excoveralls, "0.13.0", "4e1b7cc4e0351d8d16e9be21b0345a7e165798ee5319c7800b9138ce17e0b38e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "fe2a56c8909564e2e6764765878d7d5e141f2af3bc8ff3b018a68ee2a218fced"}, + "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"}, "exgravatar": {:hex, :exgravatar, "2.0.2", "638412896170409da114f98947d3f8d4f38e851b0e329c1cc4cd324d5e2ea081", [:mix], [], "hexpm", "f3deb5baa6fcf354a965d794ee73a956d95f1f79f41bddf69800c713cfb014a1"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, - "exvcr": {:hex, :exvcr, "0.11.1", "a5e5f57a67538e032e16cfea6cfb1232314fb146e3ceedf1cde4a11f12fb7a58", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "984a4d52d9e01d5f0e28d45718565a41dffab3ac18e029ae45d42f16a2a58a1d"}, + "exvcr": {:hex, :exvcr, "0.11.2", "24aec6ad13a659f10591911089c01f8d2691e2fff75710c924b64437cc1b36a1", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "0dad8a3065af4040933bc3ec296f28654b04e993a81054199c832fa86329e80f"}, "fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"}, "fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"}, "file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"}, "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, - "floki": {:hex, :floki, "0.26.0", "4df88977e2e357c6720e1b650f613444bfb48c5acfc6a0c646ab007d08ad13bf", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e7b66ce7feef5518a9cd9fc7b52dd62a64028bd9cb6d6ad282a0f0fc90a4ae52"}, + "floki": {:hex, :floki, "0.27.0", "6b29a14283f1e2e8fad824bc930eaa9477c462022075df6bea8f0ad811c13599", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "583b8c13697c37179f1f82443bcc7ad2f76fbc0bf4c186606eebd658f7f2631b"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "geo": {:hex, :geo, "3.3.3", "1119302b20d21515fbcec0a180b82653524067873ed333e7fa1f55e39959d702", [:mix], [], "hexpm", "8297ae0ac5ce47bb608b2bc8a63030460020ae537de9464a7a652f25baf6d2c1"}, "geo_postgis": {:hex, :geo_postgis, "3.3.1", "45bc96b9121d0647341685dc9d44956d61338707482d655c803500676b0413a1", [:mix], [{:geo, "~> 3.3", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "3c3957d8750e3effd565f068ee658ef0e881f9a07084a23f6c5ef8262d09b8e9"}, @@ -67,6 +68,8 @@ "guardian_db": {:hex, :guardian_db, "2.0.3", "18c847efbf7ec3c0dd44c7aecaeeb2777588bbb8d2073ffc36e71037108b3be6", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "17306e09498bca379fb8eded2ac44d7690f738ca14b17080d06a948d034ea087"}, "guardian_phoenix": {:hex, :guardian_phoenix, "2.0.1", "89a817265af09a6ddf7cb1e77f17ffca90cea2db10ff888375ef34502b2731b1", [:mix], [{:guardian, "~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "21f439246715192b231f228680465d1ed5fbdf01555a4a3b17165532f5f9a08c"}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, + "hammox": {:hex, :hammox, "0.2.5", "55436c392c242ae893ebddda8ad20bafb3a5fd6d9899dd44dbf29b84420cf316", [:mix], [{:mox, "~> 0.5", [hex: :mox, repo: "hexpm", optional: false]}, {:ordinal, "~> 0.1", [hex: :ordinal, repo: "hexpm", optional: false]}], "hexpm", "c4862a86eeec8531f14795b584677870c58a4c8de5eab5730904db3a27b836f2"}, + "hashids": {:hex, :hashids, "2.0.4", "ea47a2c2018b7ffb4f5ac9b0f8ea0af6d6159b9e190c5ed09f0ea83276968e0f", [:mix], [], "hexpm", "812e2c7ae763609a47acdd4c64d58c72f63bcfd741e6c605127e43af1507e019"}, "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.0", "0310d27d7bafb662f30bff22ec732a72414799c83eaf44239781fd23b96216c0", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "c5d79626be0b6e50c19ecdfb783ee26e85bd3a77436b488379ce6dc104ec4593"}, "http_sign": {:hex, :http_sign, "0.1.1", "b16edb83aa282892f3271f9a048c155e772bf36e15700ab93901484c55f8dd10", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2d4b1c2579d85534035f12c9e1260abdf6d03a9ad4f515b2ee53b50e68c8b787"}, @@ -92,13 +95,15 @@ "mochiweb": {:hex, :mochiweb, "2.20.1", "e4dbd0ed716f076366ecf62ada5755a844e1d95c781e8c77df1d4114be868cdf", [:rebar3], [], "hexpm", "d1aeee7870470d2fa9eae0b3d5ab6c33801aa2d82b10e9dade885c5c921b36aa"}, "mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"}, "mogrify": {:hex, :mogrify, "0.7.4", "9b2496dde44b1ce12676f85d7dc531900939e6367bc537c7243a1b089435b32d", [:mix], [], "hexpm", "50d79e337fba6bc95bfbef918058c90f50b17eed9537771e61d4619488f099c3"}, + "mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"}, "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"}, "oauther": {:hex, :oauther, "1.1.1", "7d8b16167bb587ecbcddd3f8792beb9ec3e7b65c1f8ebd86b8dd25318d535752", [:mix], [], "hexpm", "9374f4302045321874cccdc57eb975893643bd69c3b22bf1312dab5f06e5788e"}, "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"}, + "ordinal": {:hex, :ordinal, "0.1.0", "2f7a1a64ff4be44b8a674718bb00d1584188fe92fa2fa48b95b1e72096d74a34", [:mix], [], "hexpm", "9f3d0a50c285ac99faa9626376e11afa6fc83d42e95166768b37d176cff485a3"}, "paddle": {:hex, :paddle, "0.1.4", "3697996d79e3d771d6f7560a23e4bad1ed7b7f7fd3e784f97bc39565963b2b13", [:mix], [], "hexpm", "fc719a9e7c86f319b9f4bf413d6f0f326b0c4930d5bc6630d074598ed38e2143"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"}, + "phoenix": {:hex, :phoenix, "1.5.4", "0fca9ce7e960f9498d6315e41fcd0c80bfa6fbeb5fa3255b830c67fdfb7e703f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4e516d131fde87b568abd62e1b14aa07ba7d5edfd230bab4e25cc9dedbb39135"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"}, "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.4", "940c0344b1d66a2e46eef02af3a70e0c5bb45a4db0bf47917add271b76cd3914", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "38f9308357dea4cc77f247e216da99fcb0224e05ada1469167520bed4cb8cccd"}, @@ -112,14 +117,16 @@ "progress_bar": {:hex, :progress_bar, "2.0.0", "447285f533b4b8717881fdb7160c7360c2f2ab57276f8904ce6d40482857e573", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "9d8b879f322fd5563e8e7ec39f1d02a9da3ffc36019f05287788744e88260fde"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, "rsa_ex": {:hex, :rsa_ex, "0.4.0", "e28dd7dc5236e156df434af0e4aa822384c8866c928e17b785d4edb7c253b558", [:mix], [], "hexpm", "40e1f08e8401da4be59a6dd0f4da30c42d5bb01703161f0208d839d97db27f4e"}, + "shortuuid": {:hex, :shortuuid, "2.1.2", "14dbafdb2f6c7213fdfcc05c7572384b5051a7b1621170018ad4c05504bd96c1", [:mix], [], "hexpm", "d9b0c4f37500ea5199b6275ece872e213e9f45a015caf4aa777cec84f63ad353"}, "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"}, "slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, + "tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"}, "timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"}, "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, "ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"}, - "ueberauth_discord": {:hex, :ueberauth_discord, "0.5.0", "52421277b93fda769b51636e542b5085f3861efdc7fa48ac4bedb6dae0b645e1", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.3", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "9a3808baf44297e26bd5042ba9ea5398aa60023e054eb9a5ac8a4eacd0467a78"}, + "ueberauth_discord": {:hex, :ueberauth_discord, "0.5.1", "3403cd2da533487370698152497b6164e5e1058e29241e78cd34ce75e727a168", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.3", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "623470f46553eb902f3cc2c922c33964300152402357daa44244df432b4e4f04"}, "ueberauth_facebook": {:hex, :ueberauth_facebook, "0.8.1", "c254be4ab367c276773c2e41d3c0fe343ae118e244afc8d5a4e3e5c438951fdc", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "c2cf210ef45bd20611234ef17517f9d1dff6b31d3fb6ad96789143eb0943f540"}, "ueberauth_github": {:hex, :ueberauth_github, "0.8.0", "2216c8cdacee0de6245b422fb397921b64a29416526985304e345dab6a799d17", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "b65ccc001a7b0719ba069452f3333d68891f4613ae787a340cce31e2a43307a3"}, "ueberauth_gitlab_strategy": {:git, "https://github.com/tcitworld/ueberauth_gitlab.git", "9fc5d30b5d87ff7cdef293a1c128f25777dcbe59", [branch: "upgrade-deps"]}, diff --git a/priv/repo/migrations/20190103150805_fix_event_visibility.exs b/priv/repo/migrations/20190103150805_fix_event_visibility.exs index 7c1a3fa43..fe3e8b35e 100644 --- a/priv/repo/migrations/20190103150805_fix_event_visibility.exs +++ b/priv/repo/migrations/20190103150805_fix_event_visibility.exs @@ -4,7 +4,7 @@ defmodule Mobilizon.Repo.Migrations.FixEventVisibility do def up do Mobilizon.Events.EventVisibility.create_type() Mobilizon.Events.EventStatus.create_type() - Mobilizon.Conversations.CommentVisibility.create_type() + Mobilizon.Discussions.CommentVisibility.create_type() alter table(:events) do remove(:public) @@ -15,7 +15,7 @@ defmodule Mobilizon.Repo.Migrations.FixEventVisibility do end alter table(:comments) do - add(:visibility, Mobilizon.Conversations.CommentVisibility.type()) + add(:visibility, Mobilizon.Discussions.CommentVisibility.type()) end end @@ -34,6 +34,6 @@ defmodule Mobilizon.Repo.Migrations.FixEventVisibility do Mobilizon.Events.EventVisibility.drop_type() Mobilizon.Events.EventStatus.drop_type() - Mobilizon.Conversations.CommentVisibility.drop_type() + Mobilizon.Discussions.CommentVisibility.drop_type() end end diff --git a/priv/repo/migrations/20190929170817_rename_postgres_types.exs b/priv/repo/migrations/20190929170817_rename_postgres_types.exs index a46d0d28b..b418e6793 100644 --- a/priv/repo/migrations/20190929170817_rename_postgres_types.exs +++ b/priv/repo/migrations/20190929170817_rename_postgres_types.exs @@ -2,7 +2,7 @@ defmodule Mobilizon.Storage.Repo.Migrations.RenamePostgresTypes do use Ecto.Migration alias Mobilizon.Actors.{ActorVisibility, MemberRole} - alias Mobilizon.Conversations.CommentVisibility + alias Mobilizon.Discussions.CommentVisibility alias Mobilizon.Events.{ JoinOptions, diff --git a/priv/repo/migrations/20200708080516_create_posts.exs b/priv/repo/migrations/20200708080516_create_posts.exs new file mode 100644 index 000000000..59c068449 --- /dev/null +++ b/priv/repo/migrations/20200708080516_create_posts.exs @@ -0,0 +1,47 @@ +defmodule Mobilizon.Repo.Migrations.CreatePosts do + use Ecto.Migration + + alias Mobilizon.Posts.PostVisibility + + def up do + PostVisibility.create_type() + + create table(:posts, primary_key: false) do + add(:id, :uuid, primary_key: true) + add(:title, :string) + add(:slug, :string) + add(:url, :string) + add(:body, :text) + add(:draft, :boolean, default: false, null: false) + add(:local, :boolean, default: true, null: false) + add(:visibility, PostVisibility.type(), default: "public") + add(:publish_at, :utc_datetime) + add(:author_id, references(:actors, on_delete: :delete_all)) + add(:attributed_to_id, references(:actors, on_delete: :delete_all)) + add(:picture_id, references(:pictures, on_delete: :delete_all)) + + timestamps() + end + + create table(:posts_tags, primary_key: false) do + add(:post_id, references(:posts, on_delete: :delete_all, type: :uuid), primary_key: true) + add(:tag_id, references(:tags, on_delete: :delete_all), primary_key: true) + end + + alter table(:actors) do + add(:posts_url, :string, null: true) + add(:events_url, :string, null: true) + end + end + + def down do + drop(table(:posts_tags)) + drop(table(:posts)) + PostVisibility.drop_type() + + alter table(:actors) do + remove(:posts_url, :string, null: true) + remove(:events_url, :string, null: true) + end + end +end diff --git a/priv/repo/migrations/20200717142633_rename_conversations_to_discussions.exs b/priv/repo/migrations/20200717142633_rename_conversations_to_discussions.exs new file mode 100644 index 000000000..293db1781 --- /dev/null +++ b/priv/repo/migrations/20200717142633_rename_conversations_to_discussions.exs @@ -0,0 +1,21 @@ +defmodule Mobilizon.Storage.Repo.Migrations.RenameConversationsToDiscussions do + use Ecto.Migration + + def up do + rename(table("conversations"), to: table("discussions")) + rename(table("comments"), :conversation_id, to: :discussion_id) + + alter table(:actors) do + add(:discussions_url, :string, null: true) + end + end + + def down do + rename(table("discussions"), to: table("conversations")) + rename(table("comments"), :discussion_id, to: :conversation_id) + + alter table(:actors) do + remove(:discussions_url) + end + end +end diff --git a/priv/repo/migrations/20200721131521_add_url_to_conversation.exs b/priv/repo/migrations/20200721131521_add_url_to_conversation.exs new file mode 100644 index 000000000..b0c30eefc --- /dev/null +++ b/priv/repo/migrations/20200721131521_add_url_to_conversation.exs @@ -0,0 +1,36 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddUrlToConversation do + use Ecto.Migration + + def up do + # Just in case old name is used + drop_if_exists(constraint(:comments, :comments_conversation_id_fkey)) + drop_if_exists(constraint(:comments, :comments_discussion_id_fkey)) + + alter table(:discussions, primary_key: false) do + remove(:id) + add(:id, :uuid, primary_key: true) + add(:url, :string, null: false) + end + + alter table(:comments) do + remove(:discussion_id) + add(:discussion_id, references(:discussions, type: :uuid), null: true) + end + end + + def down do + drop_if_exists(constraint(:comments, :comments_conversation_id_fkey)) + drop_if_exists(constraint(:comments, :comments_discussion_id_fkey)) + + alter table(:discussions, primary_key: true) do + remove(:id) + add(:id, :serial, primary_key: true) + remove(:url) + end + + alter table(:comments) do + remove(:discussion_id) + add(:discussion_id, references(:discussions, type: :serial), null: true) + end + end +end diff --git a/schema.graphql b/schema.graphql index 5db079707..ec4af4a81 100644 --- a/schema.graphql +++ b/schema.graphql @@ -7,21 +7,33 @@ schema { subscription: RootSubscriptionType } -"""An action log""" +""" +An action log +""" type ActionLog { - """The action that was done""" + """ + The action that was done + """ action: ActionLogAction - """The actor that acted""" + """ + The actor that acted + """ actor: Actor - """Internal ID for this comment""" + """ + Internal ID for this comment + """ id: ID - """The time when the action was performed""" + """ + The time when the action was performed + """ insertedAt: DateTime - """The object that was acted upon""" + """ + The object that was acted upon + """ object: ActionLogObject } @@ -36,78 +48,128 @@ enum ActionLogAction { REPORT_UPDATE_RESOLVED } -"""The objects that can be in an action log""" +""" +The objects that can be in an action log +""" interface ActionLogObject { - """Internal ID for this object""" + """ + Internal ID for this object + """ id: ID } -"""An ActivityPub actor""" +""" +An ActivityPub actor +""" interface Actor { - """The actor's avatar picture""" + """ + The actor's avatar picture + """ avatar: Picture - """The actor's banner picture""" + """ + The actor's banner picture + """ banner: Picture - """The actor's domain if (null if it's this instance)""" + """ + The actor's domain if (null if it's this instance) + """ domain: String - """List of followers""" + """ + List of followers + """ followers: [Follower] - """Number of followers for this actor""" + """ + Number of followers for this actor + """ followersCount: Int - """List of followings""" + """ + List of followings + """ following: [Follower] - """Number of actors following this actor""" + """ + Number of actors following this actor + """ followingCount: Int - """Internal ID for this actor""" + """ + Internal ID for this actor + """ id: ID - """If the actor is from this instance""" + """ + If the actor is from this instance + """ local: Boolean - """Whether the actors manually approves followers""" + """ + Whether the actors manually approves followers + """ manuallyApprovesFollowers: Boolean - """The actor's displayed name""" + """ + The actor's displayed name + """ name: String - """The actor's preferred username""" + """ + The actor's preferred username + """ preferredUsername: String - """The actor's summary""" + """ + The actor's summary + """ summary: String - """If the actor is suspended""" + """ + If the actor is suspended + """ suspended: Boolean - """The type of Actor (Person, Group,…)""" + """ + The type of Actor (Person, Group,…) + """ type: ActorType - """The ActivityPub actor's URL""" + """ + The ActivityPub actor's URL + """ url: String } -"""The list of types an actor can be""" +""" +The list of types an actor can be +""" enum ActorType { - """An ActivityPub Application""" + """ + An ActivityPub Application + """ APPLICATION - """An ActivityPub Group""" + """ + An ActivityPub Group + """ GROUP - """An ActivityPub Organization""" + """ + An ActivityPub Organization + """ ORGANIZATION - """An ActivityPub Person""" + """ + An ActivityPub Person + """ PERSON - """An ActivityPub Service""" + """ + An ActivityPub Service + """ SERVICE } @@ -115,17 +177,23 @@ type Address { country: String description: String - """The geocoordinates for the point where this address is""" + """ + The geocoordinates for the point where this address is + """ geom: Point id: ID - """The address's locality""" + """ + The address's locality + """ locality: String originId: String postalCode: String region: String - """The address's street name (with number)""" + """ + The address's street name (with number) + """ street: String type: String url: String @@ -135,17 +203,23 @@ input AddressInput { country: String description: String - """The geocoordinates for the point where this address is""" + """ + The geocoordinates for the point where this address is + """ geom: Point id: ID - """The address's locality""" + """ + The address's locality + """ locality: String originId: String postalCode: String region: String - """The address's street name (with number)""" + """ + The address's street name (with number) + """ street: String type: String url: String @@ -206,65 +280,100 @@ type AnonymousParticipationValidationEmail { """ Represents an application - """ type Application implements Actor { - """The actor's avatar picture""" + """ + The actor's avatar picture + """ avatar: Picture - """The actor's banner picture""" + """ + The actor's banner picture + """ banner: Picture - """The actor's domain if (null if it's this instance)""" + """ + The actor's domain if (null if it's this instance) + """ domain: String - """List of followers""" + """ + List of followers + """ followers: [Follower] - """Number of followers for this actor""" + """ + Number of followers for this actor + """ followersCount: Int - """List of followings""" + """ + List of followings + """ following: [Follower] - """Number of actors following this actor""" + """ + Number of actors following this actor + """ followingCount: Int - """Internal ID for this application""" + """ + Internal ID for this application + """ id: ID - """If the actor is from this instance""" + """ + If the actor is from this instance + """ local: Boolean - """Whether the actors manually approves followers""" + """ + Whether the actors manually approves followers + """ manuallyApprovesFollowers: Boolean - """The actor's displayed name""" + """ + The actor's displayed name + """ name: String - """The actor's preferred username""" + """ + The actor's preferred username + """ preferredUsername: String - """The actor's summary""" + """ + The actor's summary + """ summary: String - """If the actor is suspended""" + """ + If the actor is suspended + """ suspended: Boolean - """The type of Actor (Person, Group,…)""" + """ + The type of Actor (Person, Group,…) + """ type: ActorType - """The ActivityPub actor's URL""" + """ + The ActivityPub actor's URL + """ url: String } -"""A comment""" +""" +A comment +""" type Comment implements ActionLogObject { actor: Person deletedAt: DateTime event: Event - """Internal ID for this comment""" + """ + Internal ID for this comment + """ id: ID inReplyToComment: Comment insertedAt: DateTime @@ -281,25 +390,39 @@ type Comment implements ActionLogObject { visibility: CommentVisibility } -"""The list of visibility options for a comment""" +""" +The list of visibility options for a comment +""" enum CommentVisibility { - """visible only to people invited""" + """ + visible only to people invited + """ INVITE - """Visible only after a moderator accepted""" + """ + Visible only after a moderator accepted + """ MODERATED - """Visible only to people members of the group or followers of the person""" + """ + Visible only to people members of the group or followers of the person + """ PRIVATE - """Publicly listed and federated. Can be shared.""" + """ + Publicly listed and federated. Can be shared. + """ PUBLIC - """Visible only to people with the link - or invited""" + """ + Visible only to people with the link - or invited + """ UNLISTED } -"""A config object""" +""" +A config object +""" type Config { anonymous: Anonymous countryCode: String @@ -313,19 +436,27 @@ type Config { registrationsWhitelist: Boolean resourceProviders: [ResourceProvider] - """The instance's terms""" + """ + The instance's terms + """ terms(locale: String = "en"): Terms } -"""A conversation""" -type Conversation { +""" +A discussion +""" +type discussion { actor: Actor - """The comments for the conversation""" + """ + The comments for the discussion + """ comments(limit: Int = 10, page: Int = 1): PaginatedCommentList creator: Person - """Internal ID for this conversation""" + """ + Internal ID for this discussion + """ id: ID insertedAt: DateTime lastComment: Comment @@ -335,19 +466,29 @@ type Conversation { } type Dashboard { - """Last public event publish""" + """ + Last public event publish + """ lastPublicEventPublished: Event - """The number of local comments""" + """ + The number of local comments + """ numberOfComments: Int - """The number of local events""" + """ + The number of local events + """ numberOfEvents: Int - """The number of current opened reports""" + """ + The number of current opened reports + """ numberOfReports: Int - """The number of local users""" + """ + The number of local users + """ numberOfUsers: Int } @@ -359,161 +500,260 @@ be converted to UTC and any UTC offset other than 0 will be rejected. """ scalar DateTime -"""Represents a deleted feed_token""" +""" +Represents a deleted feed_token +""" type DeletedFeedToken { actor: DeletedObject user: DeletedObject } -"""Represents a deleted member""" +""" +Represents a deleted member +""" type DeletedMember { actor: DeletedObject parent: DeletedObject } -"""A struct containing the id of the deleted object""" +""" +A struct containing the id of the deleted object +""" type DeletedObject { id: ID } -"""Represents a deleted participant""" +""" +Represents a deleted participant +""" type DeletedParticipant { actor: DeletedObject event: DeletedObject id: ID } -"""An event""" +""" +An event +""" type Event implements ActionLogObject { - """Who the event is attributed to (often a group)""" + """ + Who the event is attributed to (often a group) + """ attributedTo: Actor - """Datetime for when the event begins""" + """ + Datetime for when the event begins + """ beginsOn: DateTime - """The event's category""" + """ + The event's category + """ category: String - """The comments in reply to the event""" + """ + The comments in reply to the event + """ comments: [Comment] - """When the event was created""" + """ + When the event was created + """ createdAt: DateTime - """The event's description""" + """ + The event's description + """ description: String - """Whether or not the event is a draft""" + """ + Whether or not the event is a draft + """ draft: Boolean - """Datetime for when the event ends""" + """ + Datetime for when the event ends + """ endsOn: DateTime - """Internal ID for this event""" + """ + Internal ID for this event + """ id: ID - """The event's visibility""" + """ + The event's visibility + """ joinOptions: EventJoinOptions - """Whether the event is local or not""" + """ + Whether the event is local or not + """ local: Boolean - """Online address of the event""" + """ + Online address of the event + """ onlineAddress: String - """The event options""" + """ + The event options + """ options: EventOptions - """The event's organizer (as a person)""" + """ + The event's organizer (as a person) + """ organizerActor: Actor participantStats: ParticipantStats - """The event's participants""" - participants(actorId: ID, limit: Int = 10, page: Int = 1, roles: String = ""): PaginatedParticipantList + """ + The event's participants + """ + participants( + actorId: ID + limit: Int = 10 + page: Int = 1 + roles: String = "" + ): PaginatedParticipantList - """Phone address for the event""" + """ + Phone address for the event + """ phoneAddress: String - """The type of the event's address""" + """ + The type of the event's address + """ physicalAddress: Address - """The event's picture""" + """ + The event's picture + """ picture: Picture - """When the event was published""" + """ + When the event was published + """ publishAt: DateTime - """Events related to this one""" + """ + Events related to this one + """ relatedEvents: [Event] - """The event's description's slug""" + """ + The event's description's slug + """ slug: String - """Status of the event""" + """ + Status of the event + """ status: EventStatus - """The event's tags""" + """ + The event's tags + """ tags: [Tag] - """The event's title""" + """ + The event's title + """ title: String - """When the event was last updated""" + """ + When the event was last updated + """ updatedAt: DateTime - """The ActivityPub Event URL""" + """ + The ActivityPub Event URL + """ url: String - """The Event UUID""" + """ + The Event UUID + """ uuid: UUID - """The event's visibility""" + """ + The event's visibility + """ visibility: EventVisibility } -"""The list of possible options for the event's status""" +""" +The list of possible options for the event's status +""" enum EventCommentModeration { - """Anyone can comment under the event""" + """ + Anyone can comment under the event + """ ALLOW_ALL - """No one can comment except for the admin""" + """ + No one can comment except for the admin + """ CLOSED - """Every comment has to be moderated by the admin""" + """ + Every comment has to be moderated by the admin + """ MODERATED } -"""The list of join options for an event""" +""" +The list of join options for an event +""" enum EventJoinOptions { - """Anyone can join and is automatically accepted""" + """ + Anyone can join and is automatically accepted + """ FREE - """Participants must be invited""" + """ + Participants must be invited + """ INVITE - """Manual acceptation""" + """ + Manual acceptation + """ RESTRICTED } type EventOffer { - """The price amount for this offer""" + """ + The price amount for this offer + """ price: Float - """The currency for this price offer""" + """ + The currency for this price offer + """ priceCurrency: String - """The URL to access to this offer""" + """ + The URL to access to this offer + """ url: String } input EventOfferInput { - """The price amount for this offer""" + """ + The price amount for this offer + """ price: Float - """The currency for this price offer""" + """ + The currency for this price offer + """ priceCurrency: String - """The URL to access to this offer""" + """ + The URL to access to this offer + """ url: String } @@ -523,10 +763,14 @@ type EventOptions { """ anonymousParticipation: Boolean - """The list of special attendees""" + """ + The list of special attendees + """ attendees: [String] - """The policy on public comment moderation under the event""" + """ + The policy on public comment moderation under the event + """ commentModeration: EventCommentModeration """ @@ -534,31 +778,49 @@ type EventOptions { """ hideOrganizerWhenGroupEvent: Boolean - """The maximum attendee capacity for this event""" + """ + The maximum attendee capacity for this event + """ maximumAttendeeCapacity: Int - """The list of offers to show for this event""" + """ + The list of offers to show for this event + """ offers: [EventOffer] - """The list of participation conditions to accept to join this event""" + """ + The list of participation conditions to accept to join this event + """ participationConditions: [EventParticipationCondition] - """The list of the event""" + """ + The list of the event + """ program: String - """The number of remaining seats for this event""" + """ + The number of remaining seats for this event + """ remainingAttendeeCapacity: Int - """Show event end time""" + """ + Show event end time + """ showEndTime: Boolean - """Whether or not to show the participation price""" + """ + Whether or not to show the participation price + """ showParticipationPrice: Boolean - """Whether or not to show the number of remaining seats for this event""" + """ + Whether or not to show the number of remaining seats for this event + """ showRemainingAttendeeCapacity: Boolean - """Show event start time""" + """ + Show event start time + """ showStartTime: Boolean } @@ -568,10 +830,14 @@ input EventOptionsInput { """ anonymousParticipation: Boolean = false - """The list of special attendees""" + """ + The list of special attendees + """ attendees: [String] - """The policy on public comment moderation under the event""" + """ + The policy on public comment moderation under the event + """ commentModeration: EventCommentModeration """ @@ -579,122 +845,193 @@ input EventOptionsInput { """ hideOrganizerWhenGroupEvent: Boolean - """The maximum attendee capacity for this event""" + """ + The maximum attendee capacity for this event + """ maximumAttendeeCapacity: Int - """The list of offers to show for this event""" + """ + The list of offers to show for this event + """ offers: [EventOfferInput] - """The list of participation conditions to accept to join this event""" + """ + The list of participation conditions to accept to join this event + """ participationConditions: [EventParticipationConditionInput] - """The list of the event""" + """ + The list of the event + """ program: String - """The number of remaining seats for this event""" + """ + The number of remaining seats for this event + """ remainingAttendeeCapacity: Int - """Show event end time""" + """ + Show event end time + """ showEndTime: Boolean - """Whether or not to show the participation price""" + """ + Whether or not to show the participation price + """ showParticipationPrice: Boolean - """Whether or not to show the number of remaining seats for this event""" + """ + Whether or not to show the number of remaining seats for this event + """ showRemainingAttendeeCapacity: Boolean - """Show event start time""" + """ + Show event start time + """ showStartTime: Boolean } type EventParticipationCondition { - """The content for this condition""" + """ + The content for this condition + """ content: String - """The title for this condition""" + """ + The title for this condition + """ title: String - """The URL to access this condition""" + """ + The URL to access this condition + """ url: String } input EventParticipationConditionInput { - """The content for this condition""" + """ + The content for this condition + """ content: String - """The title for this condition""" + """ + The title for this condition + """ title: String - """The URL to access this condition""" + """ + The URL to access this condition + """ url: String } -"""Search events result""" +""" +Search events result +""" type Events { - """Event elements""" + """ + Event elements + """ elements: [Event]! - """Total elements""" + """ + Total elements + """ total: Int! } -"""The list of possible options for the event's status""" +""" +The list of possible options for the event's status +""" enum EventStatus { - """The event is cancelled""" + """ + The event is cancelled + """ CANCELLED - """The event is confirmed""" + """ + The event is confirmed + """ CONFIRMED - """The event is tentative""" + """ + The event is tentative + """ TENTATIVE } -"""The list of visibility options for an event""" +""" +The list of visibility options for an event +""" enum EventVisibility { - """Visible only to people members of the group or followers of the person""" + """ + Visible only to people members of the group or followers of the person + """ PRIVATE - """Publicly listed and federated. Can be shared.""" + """ + Publicly listed and federated. Can be shared. + """ PUBLIC - """Visible only after a moderator accepted""" + """ + Visible only after a moderator accepted + """ RESTRICTED - """Visible only to people with the link - or invited""" + """ + Visible only to people with the link - or invited + """ UNLISTED } -"""Represents a participant to an event""" +""" +Represents a participant to an event +""" type FeedToken { - """The event which the actor participates in""" + """ + The event which the actor participates in + """ actor: Actor - """The role of this actor at this event""" + """ + The role of this actor at this event + """ token: String - """The actor that participates to the event""" + """ + The actor that participates to the event + """ user: User } """ Represents an actor's follower - """ type Follower { - """Which profile follows""" + """ + Which profile follows + """ actor: Actor - """Whether the follow has been approved by the target actor""" + """ + Whether the follow has been approved by the target actor + """ approved: Boolean - """When the follow was created""" + """ + When the follow was created + """ insertedAt: DateTime - """What or who the profile follows""" + """ + What or who the profile follows + """ targetActor: Actor - """When the follow was updated""" + """ + When the follow was updated + """ updatedAt: DateTime } @@ -705,97 +1042,151 @@ type Geocoding { """ Represents a group of actors - """ type Group implements Actor { - """The actor's avatar picture""" + """ + The actor's avatar picture + """ avatar: Picture - """The actor's banner picture""" + """ + The actor's banner picture + """ banner: Picture - """A list of the conversations for this group""" - conversations: PaginatedConversationList + """ + A list of the discussions for this group + """ + discussions: PaginatedDiscussionList - """The actor's domain if (null if it's this instance)""" + """ + The actor's domain if (null if it's this instance) + """ domain: String - """List of followers""" + """ + List of followers + """ followers: [Follower] - """Number of followers for this actor""" + """ + Number of followers for this actor + """ followersCount: Int - """List of followings""" + """ + List of followings + """ following: [Follower] - """Number of actors following this actor""" + """ + Number of actors following this actor + """ followingCount: Int - """Internal ID for this group""" + """ + Internal ID for this group + """ id: ID - """If the actor is from this instance""" + """ + If the actor is from this instance + """ local: Boolean - """Whether the actors manually approves followers""" + """ + Whether the actors manually approves followers + """ manuallyApprovesFollowers: Boolean - """List of group members""" + """ + List of group members + """ members: PaginatedMemberList - """The actor's displayed name""" + """ + The actor's displayed name + """ name: String - """Whether the group is opened to all or has restricted access""" + """ + Whether the group is opened to all or has restricted access + """ openness: Openness - """A list of the events this actor has organized""" + """ + A list of the events this actor has organized + """ organizedEvents: PaginatedEventList - """The actor's preferred username""" + """ + The actor's preferred username + """ preferredUsername: String - """A paginated list of the resources this group has""" + """ + A paginated list of the resources this group has + """ resources(limit: Int = 10, page: Int = 1): PaginatedResourceList - """The actor's summary""" + """ + The actor's summary + """ summary: String - """If the actor is suspended""" + """ + If the actor is suspended + """ suspended: Boolean - """A paginated list of the todo lists this group has""" + """ + A paginated list of the todo lists this group has + """ todoLists: PaginatedTodoListList - """The type of Actor (Person, Group,…)""" + """ + The type of Actor (Person, Group,…) + """ type: ActorType - """The type of group : Group, Community,…""" + """ + The type of group : Group, Community,… + """ types: GroupType - """The ActivityPub actor's URL""" + """ + The ActivityPub actor's URL + """ url: String } -"""Search groups result""" +""" +Search groups result +""" type Groups { - """Group elements""" + """ + Group elements + """ elements: [Group]! - """Total elements""" + """ + Total elements + """ total: Int! } """ The types of Group that exist - """ enum GroupType { - """A public group of many actors""" + """ + A public group of many actors + """ COMMUNITY - """A private group of persons""" + """ + A private group of persons + """ GROUP } @@ -805,15 +1196,23 @@ enum InstanceTermsType { URL } -"""A JWT and the associated user ID""" +""" +A JWT and the associated user ID +""" type Login { - """A JWT Token for this session""" + """ + A JWT Token for this session + """ accessToken: String! - """A JWT Token to refresh the access token""" + """ + A JWT Token to refresh the access token + """ refreshToken: String! - """The user associated to this session""" + """ + The user associated to this session + """ user: User! } @@ -829,19 +1228,26 @@ type Maps { """ Represents a member of a group - """ type Member { - """Which profile is member of""" + """ + Which profile is member of + """ actor: Person - """The member's ID""" + """ + The member's ID + """ id: ID - """Of which the profile is member""" + """ + Of which the profile is member + """ parent: Group - """The role of this membership""" + """ + The role of this membership + """ role: MemberRoleEnum } @@ -864,125 +1270,188 @@ scalar NaiveDateTime """ Describes how an actor is opened to follows - """ enum Openness { - """The actor can only be followed by invitation""" + """ + The actor can only be followed by invitation + """ INVITE_ONLY - """The actor needs to accept the following before it's effective""" + """ + The actor needs to accept the following before it's effective + """ MODERATED - """The actor is open to followings""" + """ + The actor is open to followings + """ OPEN } type PaginatedCommentList { - """A list of comments""" + """ + A list of comments + """ elements: [Comment] - """The total number of comments in the list""" + """ + The total number of comments in the list + """ total: Int } -type PaginatedConversationList { - """A list of conversation""" - elements: [Conversation] +type PaginatedDiscussionList { + """ + A list of discussion + """ + elements: [discussion] - """The total number of comments in the list""" + """ + The total number of comments in the list + """ total: Int } type PaginatedEventList { - """A list of events""" + """ + A list of events + """ elements: [Event] - """The total number of events in the list""" + """ + The total number of events in the list + """ total: Int } type PaginatedFollowerList { - """A list of followers""" + """ + A list of followers + """ elements: [Follower] - """The total number of elements in the list""" + """ + The total number of elements in the list + """ total: Int } type PaginatedGroupList { - """A list of groups""" + """ + A list of groups + """ elements: [Group] - """The total number of elements in the list""" + """ + The total number of elements in the list + """ total: Int } type PaginatedMemberList { - """A list of members""" + """ + A list of members + """ elements: [Member] - """The total number of elements in the list""" + """ + The total number of elements in the list + """ total: Int } type PaginatedParticipantList { - """A list of participants""" + """ + A list of participants + """ elements: [Participant] - """The total number of participants in the list""" + """ + The total number of participants in the list + """ total: Int } type PaginatedResourceList { - """A list of resources""" + """ + A list of resources + """ elements: [Resource] - """The total number of resources in the list""" + """ + The total number of resources in the list + """ total: Int } type PaginatedTodoList { - """A list of todos""" + """ + A list of todos + """ elements: [Todo] - """The total number of todos in the list""" + """ + The total number of todos in the list + """ total: Int } type PaginatedTodoListList { - """A list of todo lists""" + """ + A list of todo lists + """ elements: [TodoList] - """The total number of todo lists in the list""" + """ + The total number of todo lists in the list + """ total: Int } -"""Represents a participant to an event""" +""" +Represents a participant to an event +""" type Participant { - """The actor that participates to the event""" + """ + The actor that participates to the event + """ actor: Actor - """The event which the actor participates in""" + """ + The event which the actor participates in + """ event: Event - """The participation ID""" + """ + The participation ID + """ id: ID - """The datetime this participant was created""" + """ + The datetime this participant was created + """ insertedAt: DateTime - """The metadata associated to this participant""" + """ + The metadata associated to this participant + """ metadata: ParticipantMetadata - """The role of this actor at this event""" + """ + The role of this actor at this event + """ role: ParticipantRoleEnum } type ParticipantMetadata { - """The eventual token to leave an event when user is anonymous""" + """ + The eventual token to leave an event when user is anonymous + """ cancellationToken: String - """The eventual message the participant left""" + """ + The eventual message the participant left + """ message: String } @@ -997,140 +1466,223 @@ enum ParticipantRoleEnum { } type ParticipantStats { - """The number of administrators""" + """ + The number of administrators + """ administrator: Int - """The number of creators""" + """ + The number of creators + """ creator: Int - """The number of approved participants""" + """ + The number of approved participants + """ going: Int - """The number of moderators""" + """ + The number of moderators + """ moderator: Int - """The number of not approved participants""" + """ + The number of not approved participants + """ notApproved: Int - """The number of not confirmed participants""" + """ + The number of not confirmed participants + """ notConfirmed: Int - """The number of simple participants (excluding creators)""" + """ + The number of simple participants (excluding creators) + """ participant: Int - """The number of rejected participants""" + """ + The number of rejected participants + """ rejected: Int } """ Represents a person identity - """ type Person implements Actor { - """The actor's avatar picture""" + """ + The actor's avatar picture + """ avatar: Picture - """The actor's banner picture""" + """ + The actor's banner picture + """ banner: Picture - """The actor's domain if (null if it's this instance)""" + """ + The actor's domain if (null if it's this instance) + """ domain: String - """A list of the feed tokens for this person""" + """ + A list of the feed tokens for this person + """ feedTokens: [FeedToken] - """List of followers""" + """ + List of followers + """ followers: [Follower] - """Number of followers for this actor""" + """ + Number of followers for this actor + """ followersCount: Int - """List of followings""" + """ + List of followings + """ following: [Follower] - """Number of actors following this actor""" + """ + Number of actors following this actor + """ followingCount: Int - """Internal ID for this person""" + """ + Internal ID for this person + """ id: ID - """If the actor is from this instance""" + """ + If the actor is from this instance + """ local: Boolean - """Whether the actors manually approves followers""" + """ + Whether the actors manually approves followers + """ manuallyApprovesFollowers: Boolean - """The list of groups this person is member of""" + """ + The list of groups this person is member of + """ memberOf: [Member] - """The list of group this person is member of""" + """ + The list of group this person is member of + """ memberships: PaginatedMemberList - """The actor's displayed name""" + """ + The actor's displayed name + """ name: String - """A list of the events this actor has organized""" + """ + A list of the events this actor has organized + """ organizedEvents: [Event] - """The list of events this person goes to""" + """ + The list of events this person goes to + """ participations(eventId: ID): [Participant] - """The actor's preferred username""" + """ + The actor's preferred username + """ preferredUsername: String - """The actor's summary""" + """ + The actor's summary + """ summary: String - """If the actor is suspended""" + """ + If the actor is suspended + """ suspended: Boolean - """The type of Actor (Person, Group,…)""" + """ + The type of Actor (Person, Group,…) + """ type: ActorType - """The ActivityPub actor's URL""" + """ + The ActivityPub actor's URL + """ url: String - """The user this actor is associated to""" + """ + The user this actor is associated to + """ user: User } -"""Search persons result""" +""" +Search persons result +""" type Persons { - """Person elements""" + """ + Person elements + """ elements: [Person]! - """Total elements""" + """ + Total elements + """ total: Int! } -"""A picture""" +""" +A picture +""" type Picture { - """The picture's alternative text""" + """ + The picture's alternative text + """ alt: String - """The picture's detected content type""" + """ + The picture's detected content type + """ contentType: String - """The picture's ID""" + """ + The picture's ID + """ id: ID - """The picture's name""" + """ + The picture's name + """ name: String - """The picture's size""" + """ + The picture's size + """ size: Int - """The picture's full URL""" + """ + The picture's full URL + """ url: String } -"""An attached picture or a link to a picture""" +""" +An attached picture or a link to a picture +""" input PictureInput { picture: PictureInputObject pictureId: ID } -"""An attached picture""" +""" +An attached picture +""" input PictureInputObject { actorId: ID alt: String @@ -1139,128 +1691,208 @@ input PictureInputObject { } """ -The `Point` scalar type represents Point geographic information compliant string data, +The `Point` scalar type represents Point geographic information compliant string data, represented as floats separated by a semi-colon. The geodetic system is WGS 84 """ scalar Point -"""Token""" +""" +Token +""" type RefreshedToken { - """Generated access token""" + """ + Generated access token + """ accessToken: String! - """Generated refreshed token""" + """ + Generated refreshed token + """ refreshToken: String! } -"""A report object""" +""" +A report object +""" type Report implements ActionLogObject { - """The comments that are reported""" + """ + The comments that are reported + """ comments: [Comment] - """The comment the reporter added about this report""" + """ + The comment the reporter added about this report + """ content: String - """The event that is being reported""" + """ + The event that is being reported + """ event: Event - """The internal ID of the report""" + """ + The internal ID of the report + """ id: ID - """When the report was created""" + """ + When the report was created + """ insertedAt: DateTime - """The notes made on the event""" + """ + The notes made on the event + """ notes: [ReportNote] - """The actor that is being reported""" + """ + The actor that is being reported + """ reported: Actor - """The actor that created the report""" + """ + The actor that created the report + """ reporter: Actor - """Whether the report is still active""" + """ + Whether the report is still active + """ status: ReportStatus - """When the report was updated""" + """ + When the report was updated + """ updatedAt: DateTime - """The URI of the report""" + """ + The URI of the report + """ uri: String } -"""A report note object""" +""" +A report note object +""" type ReportNote implements ActionLogObject { - """The content of the note""" + """ + The content of the note + """ content: String - """The internal ID of the report note""" + """ + The internal ID of the report note + """ id: ID - """When the report note was created""" + """ + When the report note was created + """ insertedAt: DateTime - """The moderator who added the note""" + """ + The moderator who added the note + """ moderator: Actor - """The report on which this note is added""" + """ + The report on which this note is added + """ report: Report } -"""The list of possible statuses for a report object""" +""" +The list of possible statuses for a report object +""" enum ReportStatus { - """The report has been closed""" + """ + The report has been closed + """ CLOSED - """The report has been opened""" + """ + The report has been opened + """ OPEN - """The report has been marked as resolved""" + """ + The report has been marked as resolved + """ RESOLVED } -"""A resource""" +""" +A resource +""" type Resource { - """The resource's owner""" + """ + The resource's owner + """ actor: Actor - """Children resources in folder""" + """ + Children resources in folder + """ children: PaginatedResourceList - """The resource's creator""" + """ + The resource's creator + """ creator: Actor - """The resource's ID""" + """ + The resource's ID + """ id: ID - """The resource's creation date""" + """ + The resource's creation date + """ insertedAt: NaiveDateTime - """The resource's metadata""" + """ + The resource's metadata + """ metadata: ResourceMetadata - """The resource's parent""" + """ + The resource's parent + """ parent: Resource - """The resource's path""" + """ + The resource's path + """ path: String - """The resource's URL""" + """ + The resource's URL + """ resourceUrl: String - """The resource's summary""" + """ + The resource's summary + """ summary: String - """The resource's title""" + """ + The resource's title + """ title: String - """The resource's type (if it's a folder)""" + """ + The resource's type (if it's a folder) + """ type: String - """The resource's last update date""" + """ + The resource's last update date + """ updatedAt: NaiveDateTime - """The resource's URL""" + """ + The resource's URL + """ url: String } @@ -1268,21 +1900,29 @@ type ResourceMetadata { authorName: String authorUrl: String - """The resource's metadata description""" + """ + The resource's metadata description + """ description: String faviconUrl: String height: Int html: String - """The resource's metadata image""" + """ + The resource's metadata image + """ imageRemoteUrl: String providerName: String providerUrl: String - """The resource's metadata title""" + """ + The resource's metadata title + """ title: String - """The type of the resource""" + """ + The type of the resource + """ type: String width: Int } @@ -1294,10 +1934,14 @@ type ResourceProvider { } type RootMutationType { - """Create an user""" + """ + Create an user + """ createUser(email: String!, locale: String, password: String!): User - """Register a first profile on registration""" + """ + Register a first profile on registration + """ registerPerson( """ The avatar for the profile, either as an object or directly the ID of an existing Picture @@ -1309,52 +1953,98 @@ type RootMutationType { """ banner: PictureInput - """The email from the user previously created""" + """ + The email from the user previously created + """ email: String! - """The displayed name for the new profile""" + """ + The displayed name for the new profile + """ name: String = "" preferredUsername: String! - """The summary for the new profile""" + """ + The summary for the new profile + """ summary: String = "" ): Person - """Join a group""" + """ + Join a group + """ joinGroup(actorId: ID!, groupId: ID!): Member - """Create a Feed Token""" + """ + Create a Feed Token + """ createFeedToken(actorId: ID): FeedToken - """Get a preview for a resource link""" + """ + Get a preview for a resource link + """ previewResourceLink(resourceUrl: String!): ResourceMetadata - """Create a note on a report""" + """ + Create a note on a report + """ createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote - """Create a conversation""" - createConversation(actorId: ID!, creatorId: ID!, text: String!, title: String!): Conversation + """ + Create a discussion + """ + createDiscussion( + actorId: ID! + creatorId: ID! + text: String! + title: String! + ): discussion inviteMember(actorId: ID!, groupId: ID!, targetActorUsername: String!): Member - """Leave an event""" + """ + Leave an event + """ leaveEvent(actorId: ID!, eventId: ID!, token: String): DeletedParticipant - """Login an user""" + """ + Login an user + """ login(email: String!, password: String!): Login - """Change default actor for user""" + """ + Change default actor for user + """ changeDefaultActor(preferredUsername: String!): User - """Join an event""" - joinEvent(actorId: ID!, email: String, eventId: ID!, message: String): Participant + """ + Join an event + """ + joinEvent( + actorId: ID! + email: String + eventId: ID! + message: String + ): Participant - """Create a todo""" - createTodo(assignedToId: ID, dueDate: DateTime, status: Boolean, title: String!, todoListId: ID!): Todo + """ + Create a todo + """ + createTodo( + assignedToId: ID + dueDate: DateTime + status: Boolean + title: String! + todoListId: ID! + ): Todo - """Change an user password""" + """ + Change an user password + """ changePassword(newPassword: String!, oldPassword: String!): User - """Create a new person for user""" + """ + Create a new person for user + """ createPerson( """ The avatar for the profile, either as an object or directly the ID of an existing Picture @@ -1366,29 +2056,43 @@ type RootMutationType { """ banner: PictureInput - """The displayed name for the new profile""" + """ + The displayed name for the new profile + """ name: String = "" preferredUsername: String! - """The summary for the new profile""" + """ + The summary for the new profile + """ summary: String = "" ): Person - replyToConversation(conversationId: ID!, text: String!): Conversation + replyToDiscussion(discussionId: ID!, text: String!): discussion - """Resend registration confirmation token""" + """ + Resend registration confirmation token + """ resendConfirmationEmail(email: String!, locale: String): String - """Create a todo list""" + """ + Create a todo list + """ createTodoList(groupId: ID!, title: String!): TodoList - """Delete an event""" + """ + Delete an event + """ deleteEvent(actorId: ID!, eventId: ID!): DeletedObject - """Change an user email""" + """ + Change an user email + """ changeEmail(email: String!, password: String!): User - updateConversation(conversationId: ID!, title: String!): Conversation + updateDiscussion(discussionId: ID!, title: String!): discussion - """Create an event""" + """ + Create an event + """ createEvent( attributedToId: ID beginsOn: DateTime! @@ -1410,47 +2114,103 @@ type RootMutationType { publishAt: DateTime status: EventStatus - """The list of tags associated to the event""" + """ + The list of tags associated to the event + """ tags: [String] = [""] title: String! visibility: EventVisibility = PUBLIC ): Event - """Delete a feed token""" + """ + Delete a feed token + """ deleteFeedToken(token: String!): DeletedFeedToken - """Delete a resource""" + """ + Delete a resource + """ deleteResource(id: ID!): DeletedObject - """Reset user password""" + """ + Reset user password + """ resetPassword(locale: String = "en", password: String!, token: String!): Login deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject - """Delete an identity""" + """ + Delete an identity + """ deletePerson(id: ID!): Person - """Upload a picture""" - uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture + """ + Upload a picture + """ + uploadPicture( + actorId: ID! + alt: String + file: Upload! + name: String! + ): Picture - """Refresh a token""" + """ + Refresh a token + """ refreshToken(refreshToken: String!): RefreshedToken - """Delete a group""" + """ + Delete a group + """ deleteGroup(actorId: ID!, groupId: ID!): DeletedObject - """Update a resource""" - updateResource(actorId: ID, id: ID!, parentId: ID, path: String, resourceUrl: String, summary: String, title: String): Resource + """ + Update a resource + """ + updateResource( + actorId: ID + id: ID! + parentId: ID + path: String + resourceUrl: String + summary: String + title: String + ): Resource - """Create a comment""" - createComment(actorId: ID!, eventId: ID, inReplyToCommentId: ID, text: String!): Comment + """ + Create a comment + """ + createComment( + actorId: ID! + eventId: ID + inReplyToCommentId: ID + text: String! + ): Comment - """Create a resource""" - createResource(actorId: ID!, parentId: ID, path: String! = "/", resourceUrl: String, summary: String, title: String, type: String): Resource + """ + Create a resource + """ + createResource( + actorId: ID! + parentId: ID + path: String! = "/" + resourceUrl: String + summary: String + title: String + type: String + ): Resource - """Update a report""" - updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report + """ + Update a report + """ + updateReportStatus( + moderatorId: ID! + reportId: ID! + status: ReportStatus! + ): Report - """Update an event""" + """ + Update an event + """ updateEvent( attributedToId: ID beginsOn: DateTime @@ -1472,45 +2232,80 @@ type RootMutationType { picture: PictureInput status: EventStatus - """The list of tags associated to the event""" + """ + The list of tags associated to the event + """ tags: [String] title: String visibility: EventVisibility = PUBLIC ): Event - """Confirm a participation""" + """ + Confirm a participation + """ confirmParticipation(confirmationToken: String!): Participant - deleteConversation(conversationId: ID!): Conversation + deleteDiscussion(discussionId: ID!): discussion - """Reject a relay subscription""" + """ + Reject a relay subscription + """ rejectRelay(address: String!): Follower - """Accept a participation""" - updateParticipation(id: ID!, moderatorActorId: ID!, role: ParticipantRoleEnum!): Participant + """ + Accept a participation + """ + updateParticipation( + id: ID! + moderatorActorId: ID! + role: ParticipantRoleEnum! + ): Participant - """Delete a todo""" + """ + Delete a todo + """ deleteTodo(id: ID!): DeletedObject deleteComment(actorId: ID!, commentId: ID!): Comment - """Validate an user email""" + """ + Validate an user email + """ validateEmail(token: String!): User - """Send a link through email to reset user password""" + """ + Send a link through email to reset user password + """ sendResetPassword(email: String!, locale: String): String - """Update a comment""" + """ + Update a comment + """ updateComment(commentId: ID!, text: String!): Comment - """Accept a relay subscription""" + """ + Accept a relay subscription + """ acceptRelay(address: String!): Follower - """Create a report""" - createReport(commentsIds: [ID] = [""], content: String, eventId: ID, forward: Boolean = false, reportedId: ID!, reporterId: ID!): Report + """ + Create a report + """ + createReport( + commentsIds: [ID] = [""] + content: String + eventId: ID + forward: Boolean = false + reportedId: ID! + reporterId: ID! + ): Report - """Delete a relay subscription""" + """ + Delete a relay subscription + """ removeRelay(address: String!): Follower - """Update an identity""" + """ + Update an identity + """ updatePerson( """ The avatar for the profile, either as an object or directly the ID of an existing Picture @@ -1523,23 +2318,35 @@ type RootMutationType { banner: PictureInput id: ID! - """The displayed name for this profile""" + """ + The displayed name for this profile + """ name: String - """The summary for this profile""" + """ + The summary for this profile + """ summary: String ): Person - """Validate an user after registration""" + """ + Validate an user after registration + """ validateUser(token: String!): Login - """Leave an event""" + """ + Leave an event + """ leaveGroup(actorId: ID!, groupId: ID!): DeletedMember - """Delete an account""" + """ + Delete an account + """ deleteAccount(password: String!): DeletedObject - """Create a group""" + """ + Create a group + """ createGroup( """ The avatar for the group, either as an object or directly the ID of an existing Picture @@ -1551,145 +2358,258 @@ type RootMutationType { """ banner: PictureInput - """The identity that creates the group""" + """ + The identity that creates the group + """ creatorActorId: ID! - """The displayed name for the group""" + """ + The displayed name for the group + """ name: String - """The name for the group""" + """ + The name for the group + """ preferredUsername: String! - """The summary for the group""" + """ + The summary for the group + """ summary: String = "" ): Group - """Add a relay subscription""" + """ + Add a relay subscription + """ addRelay(address: String!): Follower - saveAdminSettings(instanceDescription: String, instanceName: String, instanceTerms: String, instanceTermsType: InstanceTermsType, instanceTermsUrl: String, registrationsOpen: Boolean): AdminSettings + saveAdminSettings( + instanceDescription: String + instanceName: String + instanceTerms: String + instanceTermsType: InstanceTermsType + instanceTermsUrl: String + registrationsOpen: Boolean + ): AdminSettings - """Update a todo""" - updateTodo(assignedToId: ID, dueDate: DateTime, id: ID!, status: Boolean, title: String, todoListId: ID): Todo + """ + Update a todo + """ + updateTodo( + assignedToId: ID + dueDate: DateTime + id: ID! + status: Boolean + title: String + todoListId: ID + ): Todo } """ Root Query - """ type RootQueryType { - """Get the list of action logs""" + """ + Get the list of action logs + """ actionLogs(limit: Int = 10, page: Int = 1): [ActionLog] adminSettings: AdminSettings - """Get the instance config""" + """ + Get the instance config + """ config: Config - """Get a conversation""" - conversation(id: ID!): Conversation + """ + Get a discussion + """ + discussion(id: ID!): discussion dashboard: Dashboard - """Get an event by uuid""" + """ + Get an event by uuid + """ event(uuid: UUID!): Event - """Get all events""" + """ + Get all events + """ events(limit: Int = 10, page: Int = 1): [Event] - """Get a person by its (federated) username""" + """ + Get a person by its (federated) username + """ fetchPerson(preferredUsername: String!): Person - """Get a group by its preferred username""" + """ + Get a group by its preferred username + """ group(preferredUsername: String!): Group - """Get all groups""" + """ + Get all groups + """ groups(limit: Int = 10, page: Int = 1): PaginatedGroupList - """Get the persons for an user""" + """ + Get the persons for an user + """ identities: [Person] - """Get the current actor for the logged-in user""" + """ + Get the current actor for the logged-in user + """ loggedPerson: Person - """Get the current user""" + """ + Get the current user + """ loggedUser: User - """Get a person by its ID""" + """ + Get a person by its ID + """ person(id: ID!): Person - """Get a picture""" + """ + Get a picture + """ picture(id: String!): Picture relayFollowers(limit: Int = 10, page: Int = 1): PaginatedFollowerList - relayFollowings(direction: String = "desc", limit: Int = 10, orderBy: String = "updated_at", page: Int = 1): PaginatedFollowerList + relayFollowings( + direction: String = "desc" + limit: Int = 10 + orderBy: String = "updated_at" + page: Int = 1 + ): PaginatedFollowerList - """Get a report by id""" + """ + Get a report by id + """ report(id: ID!): Report - """Get all reports""" + """ + Get all reports + """ reports(limit: Int = 10, page: Int = 1, status: ReportStatus = OPEN): [Report] - """Get a resource""" + """ + Get a resource + """ resource(id: ID, path: String, username: String): Resource - """Reverse geocode coordinates""" - reverseGeocode(latitude: Float!, locale: String = "en", longitude: Float!, zoom: Int = 15): [Address] + """ + Reverse geocode coordinates + """ + reverseGeocode( + latitude: Float! + locale: String = "en" + longitude: Float! + zoom: Int = 15 + ): [Address] - """Search for an address""" - searchAddress(limit: Int = 10, locale: String = "en", page: Int = 1, query: String!): [Address] + """ + Search for an address + """ + searchAddress( + limit: Int = 10 + locale: String = "en" + page: Int = 1 + query: String! + ): [Address] - """Search events""" + """ + Search events + """ searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events - """Search groups""" + """ + Search groups + """ searchGroups(limit: Int = 10, page: Int = 1, search: String!): Groups - """Search persons""" + """ + Search persons + """ searchPersons(limit: Int = 10, page: Int = 1, search: String!): Persons - """Get the list of tags""" + """ + Get the list of tags + """ tags(limit: Int = 10, page: Int = 1): [Tag]! - """Get replies for thread""" + """ + Get replies for thread + """ thread(id: ID): [Comment] - """Get a todo""" + """ + Get a todo + """ todo(id: ID!): Todo - """Get a todo list""" + """ + Get a todo list + """ todoList(id: ID!): TodoList - """Get an user""" + """ + Get an user + """ user(id: ID!): User - """List instance users""" - users(direction: SortDirection = DESC, limit: Int = 10, page: Int = 1, sort: SortableUserField = ID): Users + """ + List instance users + """ + users( + direction: SortDirection = DESC + limit: Int = 10 + page: Int = 1 + sort: SortableUserField = ID + ): Users } type RootSubscriptionType { eventPersonParticipationChanged(personId: ID!): Person } -"""The list of possible options for the event's status""" +""" +The list of possible options for the event's status +""" enum SortableUserField { ID } -"""Available sort directions""" +""" +Available sort directions +""" enum SortDirection { ASC DESC } -"""A tag""" +""" +A tag +""" type Tag { - """The tag's ID""" + """ + The tag's ID + """ id: ID - """Related tags to this tag""" + """ + Related tags to this tag + """ related: [Tag] - """The tags's slug""" + """ + The tags's slug + """ slug: String - """The tag's title""" + """ + The tag's title + """ title: String } @@ -1704,96 +2624,158 @@ type Tiles { endpoint: String } -"""A todo""" +""" +A todo +""" type Todo { - """The todos's assigned person""" + """ + The todos's assigned person + """ assignedTo: Actor - """The todo's creator""" + """ + The todo's creator + """ creator: Actor - """The todo's due date""" + """ + The todo's due date + """ dueDate: DateTime - """The todo's ID""" + """ + The todo's ID + """ id: ID - """The todo's status""" + """ + The todo's status + """ status: Boolean - """The todo's title""" + """ + The todo's title + """ title: String - """The todo list this todo is attached to""" + """ + The todo list this todo is attached to + """ todoList: TodoList } -"""A todo list""" +""" +A todo list +""" type TodoList { - """The actor that owns this todo list""" + """ + The actor that owns this todo list + """ actor: Actor - """The todo list's ID""" + """ + The todo list's ID + """ id: ID - """The todo list's title""" + """ + The todo list's title + """ title: String - """The todo-list's todos""" + """ + The todo-list's todos + """ todos: PaginatedTodoList } """ Represents an uploaded file. - """ scalar Upload -"""A local user of Mobilizon""" +""" +A local user of Mobilizon +""" type User { - """The datetime the last activation/confirmation token was sent""" + """ + The datetime the last activation/confirmation token was sent + """ confirmationSentAt: DateTime - """The account activation/confirmation token""" + """ + The account activation/confirmation token + """ confirmationToken: String - """The datetime when the user was confirmed/activated""" + """ + The datetime when the user was confirmed/activated + """ confirmedAt: DateTime - """The user's default actor""" + """ + The user's default actor + """ defaultActor: Person - """The list of draft events this user has created""" + """ + The list of draft events this user has created + """ drafts(limit: Int = 10, page: Int = 1): [Event] - """The user's email""" + """ + The user's email + """ email: String! - """A list of the feed tokens for this user""" + """ + A list of the feed tokens for this user + """ feedTokens: [FeedToken] - """The user's ID""" + """ + The user's ID + """ id: ID! - """The user's locale""" + """ + The user's locale + """ locale: String - """The list of memberships for this user""" + """ + The list of memberships for this user + """ memberships(limit: Int = 10, page: Int = 1): PaginatedMemberList - """The list of participations this user has""" - participations(afterDatetime: DateTime, beforeDatetime: DateTime, limit: Int = 10, page: Int = 1): [Participant] + """ + The list of participations this user has + """ + participations( + afterDatetime: DateTime + beforeDatetime: DateTime + limit: Int = 10 + page: Int = 1 + ): [Participant] - """The user's list of profiles (identities)""" + """ + The user's list of profiles (identities) + """ profiles: [Person]! - """The datetime last reset password email was sent""" + """ + The datetime last reset password email was sent + """ resetPasswordSentAt: DateTime - """The token sent when requesting password token""" + """ + The token sent when requesting password token + """ resetPasswordToken: String - """The role for the user""" + """ + The role for the user + """ role: UserRole } @@ -1803,12 +2785,18 @@ enum UserRole { USER } -"""Users list""" +""" +Users list +""" type Users { - """User elements""" + """ + User elements + """ elements: [User]! - """Total elements""" + """ + Total elements + """ total: Int! } diff --git a/test/federation/activity_pub/activity_pub_test.exs b/test/federation/activity_pub/activity_pub_test.exs index 6addf7a89..cb8bfda3d 100644 --- a/test/federation/activity_pub/activity_pub_test.exs +++ b/test/federation/activity_pub/activity_pub_test.exs @@ -8,9 +8,10 @@ defmodule Mobilizon.Federation.ActivityPubTest do use Mobilizon.DataCase import Mock + import Mox import Mobilizon.Factory - alias Mobilizon.{Actors, Conversations, Events} + alias Mobilizon.{Actors, Discussions, Events} alias Mobilizon.Actors.Actor alias Mobilizon.Resources.Resource alias Mobilizon.Todos.{Todo, TodoList} @@ -18,13 +19,10 @@ defmodule Mobilizon.Federation.ActivityPubTest do alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Utils alias Mobilizon.Federation.HTTPSignatures.Signature + alias Mobilizon.Service.HTTP.ActivityPub.Mock @activity_pub_public_audience "https://www.w3.org/ns/activitystreams#Public" - setup_all do - HTTPoison.start() - end - describe "setting HTTP signature" do test "set http signature header" do actor = insert(:actor) @@ -140,40 +138,75 @@ defmodule Mobilizon.Federation.ActivityPubTest do describe "fetching an" do test "object by url" do - use_cassette "activity_pub/fetch_framapiaf_framasoft_status" do - {:ok, object} = - ActivityPub.fetch_object_from_url( - "https://framapiaf.org/users/Framasoft/statuses/102093631881522097" - ) + url = "https://framapiaf.org/users/Framasoft/statuses/102093631881522097" - {:ok, object_again} = - ActivityPub.fetch_object_from_url( - "https://framapiaf.org/users/Framasoft/statuses/102093631881522097" - ) + data = + File.read!("test/fixtures/mastodon-status-2.json") + |> Jason.decode!() - assert object.id == object_again.id - end + Mock + |> expect(:call, fn + %{method: :get, url: ^url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + {:ok, object} = ActivityPub.fetch_object_from_url(url) + + {:ok, object_again} = ActivityPub.fetch_object_from_url(url) + + assert object.id == object_again.id end test "object reply by url" do - use_cassette "activity_pub/fetch_framasoft_framapiaf_reply" do - {:ok, object} = - ActivityPub.fetch_object_from_url("https://mamot.fr/@imacrea/102094441327423790") + url = "https://zoltasila.pl/objects/1c295713-8e3c-411e-9e62-57a7b9c9e514" + reply_to_url = "https://framapiaf.org/users/peertube/statuses/104584600044284729" - assert object.in_reply_to_comment.url == - "https://framapiaf.org/users/Framasoft/statuses/102093632302210150" - end + data = + File.read!("test/fixtures/mastodon-status-3.json") + |> Jason.decode!() + + reply_to_data = + File.read!("test/fixtures/mastodon-status-4.json") + |> Jason.decode!() + + Mock + |> expect(:call, 2, fn + %{method: :get, url: ^url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + + %{method: :get, url: ^reply_to_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: reply_to_data}} + end) + + {:ok, object} = ActivityPub.fetch_object_from_url(url) + + assert object.in_reply_to_comment.url == reply_to_url end test "object reply to a video by url" do - use_cassette "activity_pub/fetch_reply_to_framatube" do - {:ok, object} = - ActivityPub.fetch_object_from_url( - "https://diaspodon.fr/users/dada/statuses/100820008426311925" - ) + url = "https://diaspodon.fr/users/dada/statuses/100820008426311925" + origin_url = "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d" - assert object.in_reply_to_comment == nil - end + data = + File.read!("test/fixtures/mastodon-status-5.json") + |> Jason.decode!() + + origin_data = + File.read!("test/fixtures/peertube-video.json") + |> Jason.decode!() + + Mock + |> expect(:call, 2, fn + %{method: :get, url: ^url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + + %{method: :get, url: ^origin_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: origin_data}} + end) + + {:ok, object} = ActivityPub.fetch_object_from_url(url) + + assert object.in_reply_to_comment == nil end end @@ -181,26 +214,28 @@ defmodule Mobilizon.Federation.ActivityPubTest do test "it creates a delete activity and deletes the original event" do event = insert(:event) event = Events.get_public_event_by_url_with_preload!(event.url) - {:ok, delete, _} = ActivityPub.delete(event) + {:ok, delete, _} = ActivityPub.delete(event, event.organizer_actor) assert delete.data["type"] == "Delete" assert delete.data["actor"] == event.organizer_actor.url - assert delete.data["object"] == event.url + assert delete.data["object"]["type"] == "Event" + assert delete.data["object"]["id"] == event.url assert Events.get_event_by_url(event.url) == nil end test "it deletes the original event but only locally if needed" do - with_mock Utils, + with_mock Utils, [:passthrough], maybe_federate: fn _ -> :ok end, lazy_put_activity_defaults: fn args -> args end do event = insert(:event) event = Events.get_public_event_by_url_with_preload!(event.url) - {:ok, delete, _} = ActivityPub.delete(event, false) + {:ok, delete, _} = ActivityPub.delete(event, event.organizer_actor, false) assert delete.data["type"] == "Delete" assert delete.data["actor"] == event.organizer_actor.url - assert delete.data["object"] == event.url + assert delete.data["object"]["type"] == "Event" + assert delete.data["object"]["id"] == event.url assert delete.local == false assert Events.get_event_by_url(event.url) == nil @@ -211,15 +246,16 @@ defmodule Mobilizon.Federation.ActivityPubTest do test "it creates a delete activity and deletes the original comment" do comment = insert(:comment) - comment = Conversations.get_comment_from_url_with_preload!(comment.url) - assert is_nil(Conversations.get_comment_from_url(comment.url).deleted_at) - {:ok, delete, _} = ActivityPub.delete(comment) + comment = Discussions.get_comment_from_url_with_preload!(comment.url) + assert is_nil(Discussions.get_comment_from_url(comment.url).deleted_at) + {:ok, delete, _} = ActivityPub.delete(comment, comment.actor) assert delete.data["type"] == "Delete" assert delete.data["actor"] == comment.actor.url - assert delete.data["object"] == comment.url + assert delete.data["object"]["type"] == "Note" + assert delete.data["object"]["id"] == comment.url - refute is_nil(Conversations.get_comment_from_url(comment.url).deleted_at) + refute is_nil(Discussions.get_comment_from_url(comment.url).deleted_at) end end @@ -230,7 +266,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do actor = insert(:actor) actor_data = %{summary: @updated_actor_summary} - {:ok, update, _} = ActivityPub.update(:actor, actor, actor_data, false) + {:ok, update, _} = ActivityPub.update(actor, actor_data, false) assert update.data["actor"] == actor.url assert update.data["to"] == [@activity_pub_public_audience] @@ -246,7 +282,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do event = insert(:event, organizer_actor: actor) event_data = %{begins_on: @updated_start_time} - {:ok, update, _} = ActivityPub.update(:event, event, event_data) + {:ok, update, _} = ActivityPub.update(event, event_data) assert update.data["actor"] == actor.url assert update.data["to"] == [@activity_pub_public_audience] @@ -272,8 +308,8 @@ defmodule Mobilizon.Federation.ActivityPubTest do assert create_data.local assert create_data.data["object"]["id"] == todo_list_url assert create_data.data["object"]["type"] == "TodoList" - assert create_data.data["object"]["title"] == @todo_list_title - assert create_data.data["to"] == [group.url] + assert create_data.data["object"]["name"] == @todo_list_title + assert create_data.data["to"] == [group.members_url] assert create_data.data["actor"] == actor.url assert_called(Utils.maybe_federate(create_data)) @@ -301,7 +337,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do assert create_data.data["object"]["id"] == todo_url assert create_data.data["object"]["type"] == "Todo" assert create_data.data["object"]["name"] == @todo_title - assert create_data.data["to"] == [todo_list.actor.url] + assert create_data.data["to"] == [todo_list.actor.members_url] assert create_data.data["actor"] == actor.url assert_called(Utils.maybe_federate(create_data)) @@ -341,7 +377,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do assert create_data.data["object"]["url"] == @resource_url - assert create_data.data["to"] == [group.url] + assert create_data.data["to"] == [group.members_url] assert create_data.data["actor"] == actor.url assert create_data.data["attributedTo"] == [actor.url] @@ -372,7 +408,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do assert create_data.data["object"]["id"] == url assert create_data.data["object"]["type"] == "ResourceCollection" assert create_data.data["object"]["name"] == @folder_title - assert create_data.data["to"] == [group.url] + assert create_data.data["to"] == [group.members_url] assert create_data.data["actor"] == actor.url assert create_data.data["attributedTo"] == [actor.url] @@ -411,7 +447,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do assert create_data.data["object"]["url"] == @resource_url - assert create_data.data["to"] == [group.url] + assert create_data.data["to"] == [group.members_url] assert create_data.data["actor"] == actor.url assert create_data.data["attributedTo"] == [actor.url] @@ -437,7 +473,6 @@ defmodule Mobilizon.Federation.ActivityPubTest do {:ok, update_data, %Resource{url: url}} = ActivityPub.update( - :resource, resource, %{ title: @updated_resource_title @@ -453,7 +488,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do assert update_data.data["object"]["url"] == @resource_url - assert update_data.data["to"] == [group.url] + assert update_data.data["to"] == [group.members_url] assert update_data.data["actor"] == actor.url assert update_data.data["attributedTo"] == [actor.url] @@ -496,7 +531,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do assert update_data.data["object"]["url"] == @resource_url - assert update_data.data["to"] == [group.url] + assert update_data.data["to"] == [group.members_url] assert update_data.data["actor"] == actor.url assert update_data.data["origin"] == nil assert update_data.data["target"] == parent_url @@ -524,19 +559,23 @@ defmodule Mobilizon.Federation.ActivityPubTest do {:ok, update_data, %Resource{url: url}} = ActivityPub.delete( resource, + actor, true ) assert update_data.local assert update_data.data["type"] == "Delete" - assert update_data.data["object"] == url - assert update_data.data["to"] == [group.url] - # TODO : Add actor parameter to ActivityPub.delete/2 - # assert update_data.data["actor"] == actor.url - # assert update_data.data["attributedTo"] == [actor.url] + assert update_data.data["object"]["type"] == "Document" + assert update_data.data["object"]["id"] == url + assert update_data.data["to"] == [group.members_url] + assert update_data.data["actor"] == actor.url + assert update_data.data["attributedTo"] == [group.url] assert_called(Utils.maybe_federate(update_data)) end end end + + describe "announce" do + end end diff --git a/test/federation/activity_pub/refresher_test.exs b/test/federation/activity_pub/refresher_test.exs index 913398257..f164f5fd6 100644 --- a/test/federation/activity_pub/refresher_test.exs +++ b/test/federation/activity_pub/refresher_test.exs @@ -2,37 +2,35 @@ defmodule Mobilizon.Federation.ActivityPub.RefresherTest do use Mobilizon.DataCase alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Refresher + alias Mobilizon.Service.HTTP.ActivityPub.Mock alias Mobilizon.Web.ActivityPub.ActorView import Mobilizon.Factory - import Mock + import Mox - test "refreshes a members collection" do - %Actor{members_url: members_url, url: group_url} = group = insert(:group) - %Actor{url: actor_url} = actor = insert(:actor) - %Member{} = insert(:member, parent: group, actor: actor, role: :member) + describe "refreshes a" do + setup :verify_on_exit! - data = - ActorView.render("members.json", %{group: group, actor_applicant: actor}) |> Jason.encode!() + test "members collection" do + %Actor{members_url: members_url} = + group = + insert(:group, + url: "https://remoteinstance.tld/@group", + members_url: "https://remoteinstance.tld/@group/members", + domain: "remoteinstance.tld" + ) + + %Actor{} = actor = insert(:actor) + %Member{} = insert(:member, parent: group, actor: actor, role: :member) + + data = ActorView.render("members.json", %{actor: group, actor_applicant: actor}) + + Mock + |> expect(:call, fn + %{method: :get, url: ^members_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) - with_mocks([ - {HTTPoison, [], - [ - get!: fn ^members_url, _headers, _options -> - %HTTPoison.Response{status_code: 200, body: data} - end - ]}, - {ActivityPub, [], - [ - get_or_fetch_actor_by_url: fn url -> - case url do - ^actor_url -> {:ok, actor} - ^group_url -> {:ok, group} - end - end - ]} - ]) do assert :ok == Refresher.fetch_collection(group.members_url, actor) end end diff --git a/test/federation/activity_pub/transmogrifier/announces_test.exs b/test/federation/activity_pub/transmogrifier/announces_test.exs new file mode 100644 index 000000000..68c1eeadc --- /dev/null +++ b/test/federation/activity_pub/transmogrifier/announces_test.exs @@ -0,0 +1,173 @@ +defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.AnnouncesTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + import Mox + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions + alias Mobilizon.Discussions.{Comment, Discussion} + alias Mobilizon.Federation.ActivityPub.Transmogrifier + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Service.HTTP.ActivityPub.Mock + alias Mobilizon.Tombstone + + @comment_text "my comment" + + describe "incoming announces for discussion creation" do + setup :verify_on_exit! + + test "by group member works" do + actor = insert(:actor) + group = insert(:group) + insert(:member, parent: group, actor: actor, role: :member) + + %Comment{url: comment_url} = + comment = build(:comment, actor: actor, attributed_to: group, event: nil) + + comment_data = Convertible.model_to_as(comment) + + Mock + |> expect(:call, fn + %{method: :get, url: ^comment_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: comment_data}} + end) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Jason.decode!() + |> Map.put("actor", group.url) + |> Map.put("object", comment.url) + + {:ok, _, %Comment{actor: %Actor{url: actor_url}, url: comment_url}} = + Transmogrifier.handle_incoming(data) + + assert actor_url == comment.actor.url + + assert comment_url == comment.url + end + end + + describe "handle incoming announces for discussion updates" do + setup :verify_on_exit! + + @updated_title "Updated title" + + test "by group member works" do + actor = + insert(:actor, + domain: "otherremoteinstance.tld", + url: "http://otherremoteinstance.tld/@somemember" + ) + + group = + insert(:group, + url: "http://remoteinstance.tld/@mygroup", + domain: "remoteinstance.tld", + members_url: "http://remoteinstance.tld/@mygroup/members" + ) + + insert(:member, parent: group, actor: actor, role: :member) + + %Comment{url: _comment_url} = + comment = + insert(:comment, + actor: actor, + attributed_to: group, + text: @comment_text, + url: "http://otherremoteinstance.tld/@somemember/uuid" + ) + + %Discussion{url: discussion_url} = + discussion = + insert(:discussion, + last_comment: comment, + comments: [comment], + creator: actor, + actor: group, + url: "http://otherremoteinstance.tld/@mygroup/c/talk-of-something-sh0rt-uu1d" + ) + + discussion_updated = Map.put(discussion, :title, @updated_title) + + discussion_updated_data = Convertible.model_to_as(discussion_updated) + + Mock + |> expect(:call, fn + %{url: ^discussion_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: discussion_updated_data}} + end) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Jason.decode!() + |> Map.put("actor", group.url) + |> Map.put("object", discussion_url) + + assert {:ok, _, %Discussion{title: title}} = Transmogrifier.handle_incoming(data) + assert title == @updated_title + end + end + + describe "handle incoming announces for discussion deletion" do + setup :verify_on_exit! + + test "by group member works" do + actor = + insert(:actor, + url: "http://otherremoteinstance.tld/@somemember", + domain: "otherremoteinstance.tld" + ) + + group = + insert(:group, + url: "http://remoteinstance.tld/@mygroup", + domain: "remoteinstance.tld", + members_url: "http://remoteinstance.tld/@mygroup/members" + ) + + insert(:member, parent: group, actor: actor, role: :member) + + %Comment{url: comment_url} = + comment = + insert(:comment, + actor: actor, + attributed_to: group, + text: @comment_text, + url: "http://otherremoteinstance.tld/comment/uuid" + ) + + tombstone = build(:tombstone, uri: comment.url, actor: actor) + tombstone_data = Convertible.model_to_as(tombstone) + + Mock + |> expect(:call, fn + %{url: ^comment_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: tombstone_data}} + end) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Jason.decode!() + |> Map.put("actor", group.url) + |> Map.put("object", comment.url) + + %Comment{deleted_at: deleted_at, text: comment_text} = + Discussions.get_comment_from_url(comment.url) + + assert is_nil(deleted_at) + assert comment_text == @comment_text + + {:ok, _, %Comment{deleted_at: deleted_at, text: comment_text}} = + Transmogrifier.handle_incoming(data) + + refute is_nil(deleted_at) + refute comment_text == @comment_text + + %Tombstone{actor_id: _actor_id, uri: tombstone_uri} = Tombstone.find_tombstone(comment_url) + + # assert actor_id == comment.actor.id + + assert tombstone_uri == comment.url + end + end +end diff --git a/test/federation/activity_pub/transmogrifier/comments_test.exs b/test/federation/activity_pub/transmogrifier/comments_test.exs new file mode 100644 index 000000000..897d8eab1 --- /dev/null +++ b/test/federation/activity_pub/transmogrifier/comments_test.exs @@ -0,0 +1,155 @@ +defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.CommentsTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + import Mox + import ExUnit.CaptureLog + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions + alias Mobilizon.Discussions.Comment + alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier} + alias Mobilizon.Federation.ActivityStream.Convertible + alias Mobilizon.Service.HTTP.ActivityPub.Mock + + describe "handle incoming comments" do + setup :verify_on_exit! + + test "it ignores an incoming comment if we already have it" do + comment = insert(:comment) + comment = Repo.preload(comment, [:attributed_to]) + + activity = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "actor" => comment.actor.url, + "object" => Convertible.model_to_as(comment) + } + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + |> Map.put("object", activity["object"]) + + assert {:ok, nil, _} = Transmogrifier.handle_incoming(data) + end + + test "it fetches replied-to activities if we don't have them" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + + reply_to_url = "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94" + + object = + data["object"] + |> Map.put("inReplyTo", reply_to_url) + + data = + data + |> Map.put("object", object) + + reply_to_data = + File.read!("test/fixtures/pleroma-comment-object.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{method: :get, url: ^reply_to_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: reply_to_data}} + end) + + {:ok, returned_activity, _} = Transmogrifier.handle_incoming(data) + + %Comment{} = + origin_comment = + Discussions.get_comment_from_url( + "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94" + ) + + assert returned_activity.data["object"]["inReplyTo"] == + "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94" + + assert returned_activity.data["object"]["inReplyTo"] == origin_comment.url + end + + @url_404 "https://404.site/whatever" + test "it does not crash if the object in inReplyTo can't be fetched" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + + object = + data["object"] + |> Map.put("inReplyTo", @url_404) + + data = + data + |> Map.put("object", object) + + Mock + |> expect(:call, fn + %{method: :get, url: "https://404.site/whatever"}, _opts -> + {:ok, %Tesla.Env{status: 404, body: "Not found"}} + end) + + assert capture_log([level: :warn], fn -> + {:ok, _returned_activity, _entity} = Transmogrifier.handle_incoming(data) + end) =~ "[warn] Parent object is something we don't handle" + end + + test "it works for incoming notices" do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) + + assert data["id"] == + "https://framapiaf.org/users/admin/statuses/99512778738411822/activity" + + assert data["to"] == [ + "https://www.w3.org/ns/activitystreams#Public", + "https://framapiaf.org/users/tcit" + ] + + # assert data["cc"] == [ + # "https://framapiaf.org/users/admin/followers", + # "http://mobilizon.com/@tcit" + # ] + + assert data["actor"] == "https://framapiaf.org/users/admin" + + object = data["object"] + assert object["id"] == "https://framapiaf.org/users/admin/statuses/99512778738411822" + + assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + # assert object["cc"] == [ + # "https://framapiaf.org/users/admin/followers", + # "http://localtesting.pleroma.lol/users/lain" + # ] + + assert object["actor"] == "https://framapiaf.org/users/admin" + assert object["attributedTo"] == "https://framapiaf.org/users/admin" + + {:ok, %Actor{}} = Actors.get_actor_by_url(object["actor"]) + end + + test "it works for incoming notices with hashtags" do + data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) + assert Enum.at(data["object"]["tag"], 0)["name"] == "@tcit@framapiaf.org" + assert Enum.at(data["object"]["tag"], 1)["name"] == "#moo" + end + + test "it works for incoming notices with url not being a string (prismo)" do + data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!() + + assert {:error, :not_supported} == Transmogrifier.handle_incoming(data) + # Pages without groups are not supported + # {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + # assert data["object"]["url"] == "https://prismo.news/posts/83" + end + end +end diff --git a/test/federation/activity_pub/transmogrifier/follow_test.exs b/test/federation/activity_pub/transmogrifier/follow_test.exs new file mode 100644 index 000000000..5c15bd14c --- /dev/null +++ b/test/federation/activity_pub/transmogrifier/follow_test.exs @@ -0,0 +1,122 @@ +defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + alias Mobilizon.Actors + alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier} + + describe "handle incoming follow accept activities" do + test "it works for incoming accepts which were pre-accepted" do + follower = insert(:actor) + followed = insert(:actor) + + refute Actors.is_following(follower, followed) + + {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) + assert Actors.is_following(follower, followed) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Jason.decode!() + |> Map.put("actor", followed.url) + + object = + accept_data["object"] + |> Map.put("actor", follower.url) + |> Map.put("id", follow_activity.data["id"]) + + accept_data = Map.put(accept_data, "object", object) + + {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data) + refute activity.local + + assert activity.data["object"]["id"] == follow_activity.data["id"] + + {:ok, follower} = Actors.get_actor_by_url(follower.url) + + assert Actors.is_following(follower, followed) + end + + test "it works for incoming accepts which are referenced by IRI only" do + follower = insert(:actor) + followed = insert(:actor) + + {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Jason.decode!() + |> Map.put("actor", followed.url) + |> Map.put("object", follow_activity.data["id"]) + + {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data) + assert activity.data["object"]["id"] == follow_activity.data["id"] + assert activity.data["object"]["id"] =~ "/follow/" + assert activity.data["id"] =~ "/accept/follow/" + + {:ok, follower} = Actors.get_actor_by_url(follower.url) + + assert Actors.is_following(follower, followed) + end + + test "it fails for incoming accepts which cannot be correlated" do + follower = insert(:actor) + followed = insert(:actor) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Jason.decode!() + |> Map.put("actor", followed.url) + + accept_data = + Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url)) + + :error = Transmogrifier.handle_incoming(accept_data) + + {:ok, follower} = Actors.get_actor_by_url(follower.url) + + refute Actors.is_following(follower, followed) + end + end + + describe "handle incoming follow reject activities" do + test "it fails for incoming rejects which cannot be correlated" do + follower = insert(:actor) + followed = insert(:actor) + + accept_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Jason.decode!() + |> Map.put("actor", followed.url) + + accept_data = + Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url)) + + :error = Transmogrifier.handle_incoming(accept_data) + + {:ok, follower} = Actors.get_actor_by_url(follower.url) + + refute Actors.is_following(follower, followed) + end + + test "it works for incoming rejects which are referenced by IRI only" do + follower = insert(:actor) + followed = insert(:actor) + + {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) + + assert Actors.is_following(follower, followed) + + reject_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Jason.decode!() + |> Map.put("actor", followed.url) + |> Map.put("object", follow_activity.data["id"]) + + {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data) + + refute Actors.is_following(follower, followed) + end + end +end diff --git a/test/federation/activity_pub/transmogrifier/invite_test.exs b/test/federation/activity_pub/transmogrifier/invite_test.exs new file mode 100644 index 000000000..9f64a5351 --- /dev/null +++ b/test/federation/activity_pub/transmogrifier/invite_test.exs @@ -0,0 +1,60 @@ +defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.InviteTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + alias Mobilizon.Actors + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Federation.ActivityPub.Transmogrifier + + describe "handle Invite activities on group" do + test "it accepts Invite activities" do + %Actor{url: group_url, id: group_id} = group = insert(:group) + %Actor{url: group_admin_url, id: group_admin_id} = group_admin = insert(:actor) + + %Member{} = + _group_admin_member = + insert(:member, parent: group, actor: group_admin, role: :administrator) + + %Actor{url: invitee_url, id: invitee_id} = _invitee = insert(:actor) + + invite_data = + File.read!("test/fixtures/mobilizon-invite-activity.json") + |> Jason.decode!() + |> Map.put("actor", group_admin_url) + |> Map.put("object", group_url) + |> Map.put("target", invitee_url) + + assert {:ok, activity, %Member{}} = Transmogrifier.handle_incoming(invite_data) + assert %Member{} = member = Actors.get_member_by_url(invite_data["id"]) + assert member.actor.id == invitee_id + assert member.parent.id == group_id + assert member.role == :invited + assert member.invited_by_id == group_admin_id + end + + test "it refuses Invite activities for " do + %Actor{url: group_url, id: group_id} = group = insert(:group) + %Actor{url: group_admin_url, id: group_admin_id} = group_admin = insert(:actor) + + %Member{} = + _group_admin_member = + insert(:member, parent: group, actor: group_admin, role: :administrator) + + %Actor{url: invitee_url, id: invitee_id} = _invitee = insert(:actor) + + invite_data = + File.read!("test/fixtures/mobilizon-invite-activity.json") + |> Jason.decode!() + |> Map.put("actor", group_admin_url) + |> Map.put("object", group_url) + |> Map.put("target", invitee_url) + + assert {:ok, activity, %Member{}} = Transmogrifier.handle_incoming(invite_data) + assert %Member{} = member = Actors.get_member_by_url(invite_data["id"]) + assert member.actor.id == invitee_id + assert member.parent.id == group_id + assert member.role == :invited + assert member.invited_by_id == group_admin_id + end + end +end diff --git a/test/federation/activity_pub/transmogrifier/join_test.exs b/test/federation/activity_pub/transmogrifier/join_test.exs new file mode 100644 index 000000000..9bcbba32c --- /dev/null +++ b/test/federation/activity_pub/transmogrifier/join_test.exs @@ -0,0 +1,104 @@ +defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.JoinTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + import ExUnit.CaptureLog + alias Mobilizon.Actors.Actor + alias Mobilizon.Events + alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Transmogrifier + + describe "handle incoming join activities" do + @join_message "I want to get in!" + test "it accepts Join activities" do + %Actor{url: organizer_url} = organizer = insert(:actor) + %Actor{url: participant_url} = _participant = insert(:actor) + + %Event{url: event_url} = _event = insert(:event, organizer_actor: organizer) + + join_data = + File.read!("test/fixtures/mobilizon-join-activity.json") + |> Jason.decode!() + |> Map.put("actor", participant_url) + |> Map.put("object", event_url) + |> Map.put("participationMessage", @join_message) + + assert {:ok, activity, %Participant{} = participant} = + Transmogrifier.handle_incoming(join_data) + + assert participant.metadata.message == @join_message + assert participant.role == :participant + + assert activity.data["type"] == "Accept" + assert activity.data["object"]["object"] == event_url + assert activity.data["object"]["id"] =~ "/join/event/" + assert activity.data["object"]["type"] =~ "Join" + assert activity.data["object"]["participationMessage"] == @join_message + assert activity.data["actor"] == organizer_url + assert activity.data["id"] =~ "/accept/join/" + end + end + + describe "handle incoming accept join activities" do + test "it accepts Accept activities for Join activities" do + %Actor{url: organizer_url} = organizer = insert(:actor) + %Actor{} = participant_actor = insert(:actor) + + %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted) + + {:ok, join_activity, participation} = + ActivityPub.join(event, participant_actor, false, %{metadata: %{role: :not_approved}}) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Jason.decode!() + |> Map.put("actor", organizer_url) + |> Map.put("object", participation.url) + + {:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data) + assert accept_activity.data["object"]["id"] == join_activity.data["id"] + assert accept_activity.data["object"]["id"] =~ "/join/" + assert accept_activity.data["id"] =~ "/accept/join/" + + # We don't accept already accepted Accept activities + :error = Transmogrifier.handle_incoming(accept_data) + end + end + + describe "handle incoming reject join activities" do + test "it accepts Reject activities for Join activities" do + %Actor{url: organizer_url} = organizer = insert(:actor) + %Actor{} = participant_actor = insert(:actor) + + %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted) + + {:ok, join_activity, participation} = ActivityPub.join(event, participant_actor) + + reject_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Jason.decode!() + |> Map.put("actor", organizer_url) + |> Map.put("object", participation.url) + + {:ok, reject_activity, _} = Transmogrifier.handle_incoming(reject_data) + assert reject_activity.data["object"]["id"] == join_activity.data["id"] + assert reject_activity.data["object"]["id"] =~ "/join/" + assert reject_activity.data["id"] =~ "/reject/join/" + + # We don't accept already rejected Reject activities + assert capture_log([level: :warn], fn -> + assert :error == Transmogrifier.handle_incoming(reject_data) + end) =~ + "Unable to process Reject activity \"http://mastodon.example.org/users/admin#rejects/follows/4\". Object \"#{ + join_activity.data["id"] + }\" wasn't found." + + # Organiser is not present since we use factories directly + assert event.id + |> Events.list_participants_for_event() + |> Map.get(:elements) + |> Enum.map(& &1.role) == [:rejected] + end + end +end diff --git a/test/federation/activity_pub/transmogrifier/leave_test.exs b/test/federation/activity_pub/transmogrifier/leave_test.exs new file mode 100644 index 000000000..c72f54715 --- /dev/null +++ b/test/federation/activity_pub/transmogrifier/leave_test.exs @@ -0,0 +1,60 @@ +defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.LeaveTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + alias Mobilizon.Actors.Actor + alias Mobilizon.Events + alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Federation.ActivityPub + alias Mobilizon.Federation.ActivityPub.Transmogrifier + + describe "handle incoming leave activities on events" do + test "it accepts Leave activities" do + %Actor{url: _organizer_url} = organizer = insert(:actor) + %Actor{url: participant_url} = participant_actor = insert(:actor) + + %Event{url: event_url} = + event = insert(:event, organizer_actor: organizer, join_options: :restricted) + + organizer_participation = + %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator) + + {:ok, _join_activity, _participation} = ActivityPub.join(event, participant_actor) + + join_data = + File.read!("test/fixtures/mobilizon-leave-activity.json") + |> Jason.decode!() + |> Map.put("actor", participant_url) + |> Map.put("object", event_url) + + assert {:ok, activity, _} = Transmogrifier.handle_incoming(join_data) + + assert activity.data["object"] == event_url + assert activity.data["actor"] == participant_url + + # The only participant left is the organizer + assert event.id + |> Events.list_participants_for_event() + |> Map.get(:elements) + |> Enum.map(& &1.id) == + [organizer_participation.id] + end + + test "it refuses Leave activities when actor is the only organizer" do + %Actor{url: organizer_url} = organizer = insert(:actor) + + %Event{url: event_url} = + event = insert(:event, organizer_actor: organizer, join_options: :restricted) + + %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator) + + join_data = + File.read!("test/fixtures/mobilizon-leave-activity.json") + |> Jason.decode!() + |> Map.put("actor", organizer_url) + |> Map.put("object", event_url) + + assert :error = Transmogrifier.handle_incoming(join_data) + end + end +end diff --git a/test/federation/activity_pub/transmogrifier/undo_test.exs b/test/federation/activity_pub/transmogrifier/undo_test.exs new file mode 100644 index 000000000..53ab90667 --- /dev/null +++ b/test/federation/activity_pub/transmogrifier/undo_test.exs @@ -0,0 +1,85 @@ +defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UndoTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + import Mox + alias Mobilizon.Actors + alias Mobilizon.Discussions.Comment + alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier} + alias Mobilizon.Service.HTTP.ActivityPub.Mock + + describe "handle incoming undo activities" do + test "it works for incoming unannounces with an existing notice" do + comment = insert(:comment) + + announce_data = + File.read!("test/fixtures/mastodon-announce.json") + |> Jason.decode!() + |> Map.put("object", comment.url) + + actor_data = + File.read!("test/fixtures/mastodon-actor.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{method: :get, url: "https://framapiaf.org/users/Framasoft"}, _opts -> + {:ok, %Tesla.Env{status: 200, body: actor_data}} + end) + + {:ok, _, %Comment{}} = Transmogrifier.handle_incoming(announce_data) + + data = + File.read!("test/fixtures/mastodon-undo-announce.json") + |> Jason.decode!() + |> Map.put("object", announce_data) + |> Map.put("actor", announce_data["actor"]) + + {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + assert data["object"]["type"] == "Announce" + assert data["object"]["object"] == comment.url + + assert data["object"]["id"] == + "https://framapiaf.org/users/peertube/statuses/104584600044284729/activity" + end + + test "it works for incomming unfollows with an existing follow" do + actor = insert(:actor) + + follow_data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Jason.decode!() + |> Map.put("object", actor.url) + + actor_data = + File.read!("test/fixtures/mastodon-actor.json") + |> Jason.decode!() + |> Map.put("id", "https://social.tcit.fr/users/tcit") + + Mock + |> expect(:call, fn + %{method: :get, url: "https://social.tcit.fr/users/tcit"}, _opts -> + {:ok, %Tesla.Env{status: 200, body: actor_data}} + end) + + {:ok, %Activity{data: _, local: false}, _} = Transmogrifier.handle_incoming(follow_data) + + data = + File.read!("test/fixtures/mastodon-unfollow-activity.json") + |> Jason.decode!() + |> Map.put("object", follow_data) + + {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + assert data["object"]["type"] == "Follow" + assert data["object"]["object"] == actor.url + assert data["actor"] == "https://social.tcit.fr/users/tcit" + + {:ok, followed} = Actors.get_actor_by_url(data["actor"]) + refute Actors.is_following(followed, actor) + end + end +end diff --git a/test/federation/activity_pub/transmogrifier_test.exs b/test/federation/activity_pub/transmogrifier_test.exs index 1960c46b3..3d4cdbfe5 100644 --- a/test/federation/activity_pub/transmogrifier_test.exs +++ b/test/federation/activity_pub/transmogrifier_test.exs @@ -11,11 +11,12 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do import Mobilizon.Factory import ExUnit.CaptureLog import Mock + import Mox - alias Mobilizon.{Actors, Conversations, Events} - alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Conversations.Comment - alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.{Actors, Discussions, Events} + alias Mobilizon.Actors.Actor + alias Mobilizon.Discussions.Comment + alias Mobilizon.Events.Event alias Mobilizon.Resources.Resource alias Mobilizon.Todos.{Todo, TodoList} @@ -25,13 +26,10 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.GraphQL.API - + alias Mobilizon.Service.HTTP.ActivityPub.Mock + alias Mobilizon.Tombstone alias Mobilizon.Web.Endpoint - setup_all do - HTTPoison.start() - end - describe "handle incoming events" do test "it works for incoming events" do use_cassette "activity_pub/fetch_mobilizon_post_activity" do @@ -67,7 +65,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do assert object["actor"] == "https://test.mobilizon.org/@Alicia" assert object["location"]["name"] == "Locaux de Framasoft" - assert object["attributedTo"] == "https://test.mobilizon.org/@Alicia" + # assert object["attributedTo"] == "https://test.mobilizon.org/@Alicia" assert event.physical_address.street == "10 Rue Jangot" @@ -79,172 +77,47 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do {:ok, %Actor{}} = Actors.get_actor_by_url(object["actor"]) end end - end - describe "handle incoming comments" do - test "it ignores an incoming comment if we already have it" do - comment = insert(:comment) + test "it works for incoming events for local groups" do + %Actor{url: group_url, id: group_id} = group = insert(:group) - activity = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "actor" => comment.actor.url, - "object" => Convertible.model_to_as(comment) - } - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Jason.decode!() - |> Map.put("object", activity["object"]) - - assert {:ok, nil, _} = Transmogrifier.handle_incoming(data) - end - - test "it fetches replied-to activities if we don't have them" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Jason.decode!() - - object = - data["object"] - |> Map.put("inReplyTo", "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94") - - data = - data - |> Map.put("object", object) - - {:ok, returned_activity, _} = Transmogrifier.handle_incoming(data) - - %Comment{} = - origin_comment = - Conversations.get_comment_from_url( - "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94" + %Actor{url: actor_url, id: actor_id} = + actor = + insert(:actor, + domain: "test.mobilizon.org", + url: "https://test.mobilizon.org/@member", + preferred_username: "member" ) - assert returned_activity.data["object"]["inReplyTo"] == - "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94" + with_mock ActivityPub, [:passthrough], + get_or_fetch_actor_by_url: fn url -> + case url do + ^group_url -> {:ok, group} + ^actor_url -> {:ok, actor} + end + end do + data = File.read!("test/fixtures/mobilizon-post-activity-group.json") |> Jason.decode!() - assert returned_activity.data["object"]["inReplyTo"] == origin_comment.url - end + object = + data["object"] |> Map.put("actor", actor_url) |> Map.put("attributedTo", group_url) - test "it does not crash if the object in inReplyTo can't be fetched" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() + data = + data + |> Map.put("actor", actor_url) + |> Map.put("attributedTo", group_url) + |> Map.put("object", object) - object = - data["object"] - |> Map.put("inReplyTo", "https://404.site/whatever") + assert {:ok, %Activity{data: activity_data, local: false}, %Event{} = event} = + Transmogrifier.handle_incoming(data) - data = - data - |> Map.put("object", object) - - assert capture_log([level: :warn], fn -> - {:ok, _returned_activity, _entity} = Transmogrifier.handle_incoming(data) - end) =~ "[warn] Parent object is something we don't handle" - end - - test "it works for incoming notices" do - use_cassette "activity_pub/mastodon_post_activity" do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() - - {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - - assert data["id"] == - "https://framapiaf.org/users/admin/statuses/99512778738411822/activity" - - assert data["to"] == [ - "https://www.w3.org/ns/activitystreams#Public", - "https://framapiaf.org/users/tcit" - ] - - # assert data["cc"] == [ - # "https://framapiaf.org/users/admin/followers", - # "http://mobilizon.com/@tcit" - # ] - - assert data["actor"] == "https://framapiaf.org/users/admin" - - object = data["object"] - assert object["id"] == "https://framapiaf.org/users/admin/statuses/99512778738411822" - - assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - - # assert object["cc"] == [ - # "https://framapiaf.org/users/admin/followers", - # "http://localtesting.pleroma.lol/users/lain" - # ] - - assert object["actor"] == "https://framapiaf.org/users/admin" - assert object["attributedTo"] == "https://framapiaf.org/users/admin" - - {:ok, %Actor{}} = Actors.get_actor_by_url(object["actor"]) + assert event.organizer_actor_id == actor_id + assert event.attributed_to_id == group_id + assert activity_data["actor"] == actor_url + assert activity_data["attributedTo"] == group_url + assert activity_data["object"]["actor"] == actor_url + assert activity_data["object"]["attributedTo"] == group_url end end - - test "it works for incoming notices with hashtags" do - use_cassette "activity_pub/mastodon_activity_hashtag" do - data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!() - - {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - assert Enum.at(data["object"]["tag"], 0)["name"] == "@tcit@framapiaf.org" - assert Enum.at(data["object"]["tag"], 1)["name"] == "#moo" - end - end - - # test "it works for incoming notices with contentMap" do - # data = - # File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Jason.decode!() - - # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - - # assert data["object"]["content"] == - # "

    @lain

    " - # end - - # test "it works for incoming notices with to/cc not being an array (kroeg)" do - # data = File.read!("test/fixtures/kroeg-post-activity.json") |> Jason.decode!() - - # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - - # assert data["object"]["content"] == - # "

    henlo from my Psion netBook

    message sent from my Psion netBook

    " - # end - - # test "it works for incoming announces with actor being inlined (kroeg)" do - # data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Jason.decode!() - - # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - - # assert data["actor"] == "https://puckipedia.com/" - # end - - # test "it works for incoming notices with tag not being an array (kroeg)" do - # data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Jason.decode!() - - # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - - # assert data["object"]["emoji"] == %{ - # "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png" - # } - - # data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Jason.decode!() - - # {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - - # assert "test" in data["object"]["tag"] - # end - - test "it works for incoming notices with url not being a string (prismo)" do - data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!() - - assert {:error, :not_supported} == Transmogrifier.handle_incoming(data) - # Pages are not supported - # {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - # assert data["object"]["url"] == "https://prismo.news/posts/83" - end end describe "handle incoming todo lists" do @@ -463,7 +336,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do activity = %{ "type" => "Add", - "to" => [group.url], + "to" => [group.members_url], "actor" => actor.url, "target" => group.resources_url, "object" => Convertible.model_to_as(resource) @@ -491,7 +364,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do activity = %{ "type" => "Add", - "to" => [group.url], + "to" => [group.members_url], "actor" => creator.url, "target" => group.resources_url, "object" => %{ @@ -534,7 +407,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do activity = %{ "type" => "Add", - "to" => [group.url], + "to" => [group.members_url], "actor" => creator.url, "target" => group.resources_url, "object" => %{ @@ -586,7 +459,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do activity = %{ "type" => "Add", - "to" => [group.url], + "to" => [group.members_url], "actor" => creator.url, "target" => parent_resource.url, "object" => %{ @@ -631,7 +504,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do activity = %{ "type" => "Add", - "to" => [group.url], + "to" => [group.members_url], "actor" => creator.url, "target" => group.resources_url, "object" => %{ @@ -665,7 +538,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do activity = %{ "type" => "Add", - "to" => [group.url], + "to" => [group.members_url], "actor" => creator.url, "target" => group.resources_url, "object" => %{ @@ -787,43 +660,49 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do describe "handle incoming follow announces" do test "it works for incoming announces" do - use_cassette "activity_pub/mastodon_announce_activity" do - data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!() + data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!() + status_data = File.read!("test/fixtures/mastodon-status.json") |> Jason.decode!() - {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) + Mock + |> expect(:call, fn + %{method: :get, url: "https://framapiaf.org/users/peertube/statuses/104584600044284729"}, + _opts -> + {:ok, %Tesla.Env{status: 200, body: status_data}} + end) - assert data["actor"] == "https://framapiaf.org/users/Framasoft" - assert data["type"] == "Announce" + {:ok, _, %Comment{actor: %Actor{url: actor_url}, url: comment_url}} = + Transmogrifier.handle_incoming(data) - assert data["id"] == - "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity" + assert actor_url == "https://framapiaf.org/users/peertube" - assert data["object"] == - "https://framapiaf.org/users/Framasoft/statuses/102501959686438400" - - assert %Comment{} = Conversations.get_comment_from_url(data["object"]) - end + assert comment_url == + "https://framapiaf.org/users/peertube/statuses/104584600044284729" end test "it works for incoming announces with an existing activity" do - use_cassette "activity_pub/mastodon_announce_existing_activity" do - comment = insert(:comment) + %Comment{url: comment_url, actor: %Actor{url: actor_url} = actor} = insert(:comment) - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Jason.decode!() - |> Map.put("object", comment.url) + actor_data = + File.read!("test/fixtures/mastodon-actor.json") + |> Jason.decode!() - {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Jason.decode!() + |> Map.put("object", comment_url) - assert data["actor"] == "https://framapiaf.org/users/Framasoft" - assert data["type"] == "Announce" + Mock + |> expect(:call, fn + %{method: :get, url: actor_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: actor_data}} + end) - assert data["id"] == - "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity" + {:ok, _, %Comment{actor: %Actor{url: actor_url}, url: comment_url_2}} = + Transmogrifier.handle_incoming(data) - assert data["object"] == comment.url - end + assert actor_url == actor.url + + assert comment_url == comment_url_2 end end @@ -926,12 +805,12 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do |> Map.put("object", object) |> Map.put("actor", actor_url) - assert Conversations.get_comment_from_url(comment_url) - assert is_nil(Conversations.get_comment_from_url(comment_url).deleted_at) + assert Discussions.get_comment_from_url(comment_url) + assert is_nil(Discussions.get_comment_from_url(comment_url).deleted_at) {:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(data) - refute is_nil(Conversations.get_comment_from_url(comment_url).deleted_at) + refute is_nil(Discussions.get_comment_from_url(comment_url).deleted_at) end test "it fails for incoming deletes with spoofed origin" do @@ -942,7 +821,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do |> Jason.decode!() |> Map.put("object", comment.url) - {:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(announce_data) + {:ok, _, _} = Transmogrifier.handle_incoming(announce_data) data = File.read!("test/fixtures/mastodon-delete.json") @@ -958,9 +837,11 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do :error = Transmogrifier.handle_incoming(data) - assert Conversations.get_comment_from_url(comment.url) + assert Discussions.get_comment_from_url(comment.url) end + setup :set_mox_from_context + test "it works for incoming actor deletes" do %Actor{url: url} = actor = insert(:actor, url: "https://framapiaf.org/users/admin") %Event{url: event1_url} = event1 = insert(:event, organizer_actor: actor) @@ -971,7 +852,13 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do data = File.read!("test/fixtures/mastodon-delete-user.json") - |> Poison.decode!() + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{method: :get, url: "https://framapiaf.org/users/admin"}, _opts -> + {:ok, %Tesla.Env{status: 410, body: "Gone"}} + end) {:ok, _activity, _actor} = Transmogrifier.handle_incoming(data) assert %{success: 1, failure: 0} == Oban.drain_queue(:background) @@ -980,19 +867,31 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do assert {:error, :event_not_found} = Events.get_event(event1.id) # Tombstone are cascade deleted, seems correct for now # assert %Tombstone{} = Tombstone.find_tombstone(event1_url) - assert %Comment{deleted_at: deleted_at} = Conversations.get_comment(comment1.id) + assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id) refute is_nil(deleted_at) # assert %Tombstone{} = Tombstone.find_tombstone(comment1_url) end test "it fails for incoming actor deletes with spoofed origin" do %{url: url} = insert(:actor) + deleted_actor_url = "https://framapiaf.org/users/admin" data = File.read!("test/fixtures/mastodon-delete-user.json") - |> Poison.decode!() + |> Jason.decode!() |> Map.put("actor", url) + deleted_actor_data = + File.read!("test/fixtures/mastodon-actor.json") + |> Jason.decode!() + |> Map.put("id", deleted_actor_url) + + Mock + |> expect(:call, fn + %{url: ^deleted_actor_url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: deleted_actor_data}} + end) + assert capture_log(fn -> assert :error == Transmogrifier.handle_incoming(data) end) =~ "Object origin check failed" @@ -1001,62 +900,29 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do end end - describe "handle incoming undo activities" do - test "it works for incoming unannounces with an existing notice" do - use_cassette "activity_pub/mastodon_unannounce_activity" do - comment = insert(:comment) + describe "handle tombstones" do + setup :verify_on_exit! - announce_data = - File.read!("test/fixtures/mastodon-announce.json") - |> Jason.decode!() - |> Map.put("object", comment.url) + # This is a hack to handle fetching tombstones + test "works for incoming tombstone creations" do + %Comment{url: comment_url} = comment = insert(:comment, local: false) + tombstone = build(:tombstone, uri: comment_url) + data = Convertible.model_to_as(tombstone) - {:ok, %Activity{data: announce_data, local: false}, _} = - Transmogrifier.handle_incoming(announce_data) + activity = %{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["actor"], + "attributedTo" => data["attributedTo"], + "object" => data + } - data = - File.read!("test/fixtures/mastodon-undo-announce.json") - |> Jason.decode!() - |> Map.put("object", announce_data) - |> Map.put("actor", announce_data["actor"]) - - {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - assert data["object"]["type"] == "Announce" - assert data["object"]["object"] == comment.url - - assert data["object"]["id"] == - "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity" - end - end - - test "it works for incomming unfollows with an existing follow" do - use_cassette "activity_pub/unfollow_existing_follow_activity" do - actor = insert(:actor) - - follow_data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Jason.decode!() - |> Map.put("object", actor.url) - - {:ok, %Activity{data: _, local: false}, _} = Transmogrifier.handle_incoming(follow_data) - - data = - File.read!("test/fixtures/mastodon-unfollow-activity.json") - |> Jason.decode!() - |> Map.put("object", follow_data) - - {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - assert data["object"]["type"] == "Follow" - assert data["object"]["object"] == actor.url - assert data["actor"] == "https://social.tcit.fr/users/tcit" - - {:ok, followed} = Actors.get_actor_by_url(data["actor"]) - refute Actors.is_following(followed, actor) - end + {:ok, _activity, %Comment{url: comment_url}} = Transmogrifier.handle_incoming(activity) + assert comment_url == comment.url + assert %Comment{} = comment = Discussions.get_comment_from_url(comment_url) + assert %Tombstone{} = Tombstone.find_tombstone(comment_url) + refute is_nil(comment.deleted_at) end end @@ -1136,120 +1002,6 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do # refute User.blocks?(blocker, user) # end - describe "handle incoming follow accept activities" do - test "it works for incoming accepts which were pre-accepted" do - follower = insert(:actor) - followed = insert(:actor) - - refute Actors.is_following(follower, followed) - - {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) - assert Actors.is_following(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Jason.decode!() - |> Map.put("actor", followed.url) - - object = - accept_data["object"] - |> Map.put("actor", follower.url) - |> Map.put("id", follow_activity.data["id"]) - - accept_data = Map.put(accept_data, "object", object) - - {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data) - refute activity.local - - assert activity.data["object"]["id"] == follow_activity.data["id"] - - {:ok, follower} = Actors.get_actor_by_url(follower.url) - - assert Actors.is_following(follower, followed) - end - - test "it works for incoming accepts which are referenced by IRI only" do - follower = insert(:actor) - followed = insert(:actor) - - {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Jason.decode!() - |> Map.put("actor", followed.url) - |> Map.put("object", follow_activity.data["id"]) - - {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data) - assert activity.data["object"]["id"] == follow_activity.data["id"] - assert activity.data["object"]["id"] =~ "/follow/" - assert activity.data["id"] =~ "/accept/follow/" - - {:ok, follower} = Actors.get_actor_by_url(follower.url) - - assert Actors.is_following(follower, followed) - end - - test "it fails for incoming accepts which cannot be correlated" do - follower = insert(:actor) - followed = insert(:actor) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Jason.decode!() - |> Map.put("actor", followed.url) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url)) - - :error = Transmogrifier.handle_incoming(accept_data) - - {:ok, follower} = Actors.get_actor_by_url(follower.url) - - refute Actors.is_following(follower, followed) - end - end - - describe "handle incoming follow reject activities" do - test "it fails for incoming rejects which cannot be correlated" do - follower = insert(:actor) - followed = insert(:actor) - - accept_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Jason.decode!() - |> Map.put("actor", followed.url) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url)) - - :error = Transmogrifier.handle_incoming(accept_data) - - {:ok, follower} = Actors.get_actor_by_url(follower.url) - - refute Actors.is_following(follower, followed) - end - - test "it works for incoming rejects which are referenced by IRI only" do - follower = insert(:actor) - followed = insert(:actor) - - {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) - - assert Actors.is_following(follower, followed) - - reject_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Jason.decode!() - |> Map.put("actor", followed.url) - |> Map.put("object", follow_activity.data["id"]) - - {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data) - - refute Actors.is_following(follower, followed) - end - end - describe "handle incoming flag activities" do test "it accepts Flag activities" do %Actor{url: reporter_url} = Relay.get_actor() @@ -1276,201 +1028,6 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do end end - describe "handle incoming join activities" do - @join_message "I want to get in!" - test "it accepts Join activities" do - %Actor{url: organizer_url} = organizer = insert(:actor) - %Actor{url: participant_url} = _participant = insert(:actor) - - %Event{url: event_url} = _event = insert(:event, organizer_actor: organizer) - - join_data = - File.read!("test/fixtures/mobilizon-join-activity.json") - |> Jason.decode!() - |> Map.put("actor", participant_url) - |> Map.put("object", event_url) - |> Map.put("participationMessage", @join_message) - - assert {:ok, activity, %Participant{} = participant} = - Transmogrifier.handle_incoming(join_data) - - assert participant.metadata.message == @join_message - assert participant.role == :participant - - assert activity.data["type"] == "Accept" - assert activity.data["object"]["object"] == event_url - assert activity.data["object"]["id"] =~ "/join/event/" - assert activity.data["object"]["type"] =~ "Join" - assert activity.data["object"]["participationMessage"] == @join_message - assert activity.data["actor"] == organizer_url - assert activity.data["id"] =~ "/accept/join/" - end - end - - describe "handle incoming accept join activities" do - test "it accepts Accept activities for Join activities" do - %Actor{url: organizer_url} = organizer = insert(:actor) - %Actor{} = participant_actor = insert(:actor) - - %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted) - - {:ok, join_activity, participation} = - ActivityPub.join(event, participant_actor, false, %{metadata: %{role: :not_approved}}) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Jason.decode!() - |> Map.put("actor", organizer_url) - |> Map.put("object", participation.url) - - {:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data) - assert accept_activity.data["object"]["id"] == join_activity.data["id"] - assert accept_activity.data["object"]["id"] =~ "/join/" - assert accept_activity.data["id"] =~ "/accept/join/" - - # We don't accept already accepted Accept activities - :error = Transmogrifier.handle_incoming(accept_data) - end - end - - describe "handle incoming reject join activities" do - test "it accepts Reject activities for Join activities" do - %Actor{url: organizer_url} = organizer = insert(:actor) - %Actor{} = participant_actor = insert(:actor) - - %Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted) - - {:ok, join_activity, participation} = ActivityPub.join(event, participant_actor) - - reject_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Jason.decode!() - |> Map.put("actor", organizer_url) - |> Map.put("object", participation.url) - - {:ok, reject_activity, _} = Transmogrifier.handle_incoming(reject_data) - assert reject_activity.data["object"]["id"] == join_activity.data["id"] - assert reject_activity.data["object"]["id"] =~ "/join/" - assert reject_activity.data["id"] =~ "/reject/join/" - - # We don't accept already rejected Reject activities - assert capture_log([level: :warn], fn -> - assert :error == Transmogrifier.handle_incoming(reject_data) - end) =~ - "Unable to process Reject activity \"http://mastodon.example.org/users/admin#rejects/follows/4\". Object \"#{ - join_activity.data["id"] - }\" wasn't found." - - # Organiser is not present since we use factories directly - assert event.id - |> Events.list_participants_for_event() - |> Map.get(:elements) - |> Enum.map(& &1.role) == [:rejected] - end - end - - describe "handle incoming leave activities on events" do - test "it accepts Leave activities" do - %Actor{url: _organizer_url} = organizer = insert(:actor) - %Actor{url: participant_url} = participant_actor = insert(:actor) - - %Event{url: event_url} = - event = insert(:event, organizer_actor: organizer, join_options: :restricted) - - organizer_participation = - %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator) - - {:ok, _join_activity, _participation} = ActivityPub.join(event, participant_actor) - - join_data = - File.read!("test/fixtures/mobilizon-leave-activity.json") - |> Jason.decode!() - |> Map.put("actor", participant_url) - |> Map.put("object", event_url) - - assert {:ok, activity, _} = Transmogrifier.handle_incoming(join_data) - - assert activity.data["object"] == event_url - assert activity.data["actor"] == participant_url - - # The only participant left is the organizer - assert event.id - |> Events.list_participants_for_event() - |> Map.get(:elements) - |> Enum.map(& &1.id) == - [organizer_participation.id] - end - - test "it refuses Leave activities when actor is the only organizer" do - %Actor{url: organizer_url} = organizer = insert(:actor) - - %Event{url: event_url} = - event = insert(:event, organizer_actor: organizer, join_options: :restricted) - - %Participant{} = insert(:participant, event: event, actor: organizer, role: :creator) - - join_data = - File.read!("test/fixtures/mobilizon-leave-activity.json") - |> Jason.decode!() - |> Map.put("actor", organizer_url) - |> Map.put("object", event_url) - - assert :error = Transmogrifier.handle_incoming(join_data) - end - end - - describe "handle Invite activities on group" do - test "it accepts Invite activities" do - %Actor{url: group_url, id: group_id} = group = insert(:group) - %Actor{url: group_admin_url, id: group_admin_id} = group_admin = insert(:actor) - - %Member{} = - _group_admin_member = - insert(:member, parent: group, actor: group_admin, role: :administrator) - - %Actor{url: invitee_url, id: invitee_id} = _invitee = insert(:actor) - - invite_data = - File.read!("test/fixtures/mobilizon-invite-activity.json") - |> Jason.decode!() - |> Map.put("actor", group_admin_url) - |> Map.put("object", group_url) - |> Map.put("target", invitee_url) - - assert {:ok, activity, %Member{}} = Transmogrifier.handle_incoming(invite_data) - assert %Member{} = member = Actors.get_member_by_url(invite_data["id"]) - assert member.actor.id == invitee_id - assert member.parent.id == group_id - assert member.role == :invited - assert member.invited_by_id == group_admin_id - end - - test "it refuses Invite activities for " do - %Actor{url: group_url, id: group_id} = group = insert(:group) - %Actor{url: group_admin_url, id: group_admin_id} = group_admin = insert(:actor) - - %Member{} = - _group_admin_member = - insert(:member, parent: group, actor: group_admin, role: :administrator) - - %Actor{url: invitee_url, id: invitee_id} = _invitee = insert(:actor) - - invite_data = - File.read!("test/fixtures/mobilizon-invite-activity.json") - |> Jason.decode!() - |> Map.put("actor", group_admin_url) - |> Map.put("object", group_url) - |> Map.put("target", invitee_url) - - assert {:ok, activity, %Member{}} = Transmogrifier.handle_incoming(invite_data) - assert %Member{} = member = Actors.get_member_by_url(invite_data["id"]) - assert member.actor.id == invitee_id - assert member.parent.id == group_id - assert member.role == :invited - assert member.invited_by_id == group_admin_id - end - end - describe "prepare outgoing" do test "it turns mentions into tags" do actor = insert(:actor) @@ -1501,7 +1058,7 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do assert Enum.member?(object["tag"], expected_mention) end - test "it adds the json-ld context and the conversation property" do + test "it adds the json-ld context and the discussion property" do actor = insert(:actor) {:ok, activity, _} = API.Comments.create_comment(%{actor_id: actor.id, text: "hey"}) @@ -1558,24 +1115,40 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do describe "actor origin check" do test "it rejects objects with a bogus origin" do - use_cassette "activity_pub/object_bogus_origin" do - {:error, _} = ActivityPub.fetch_object_from_url("https://info.pleroma.site/activity.json") - end + data = + File.read!("test/fixtures/https__info.pleroma.site_activity.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{method: :get, url: "https://info.pleroma.site/activity.json"}, _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + {:error, _} = ActivityPub.fetch_object_from_url("https://info.pleroma.site/activity.json") end test "it rejects activities which reference objects with bogus origins" do - use_cassette "activity_pub/activity_object_bogus" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "https://framapiaf.org/users/admin/activities/1234", - "actor" => "https://framapiaf.org/users/admin", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => "https://info.pleroma.site/activity.json", - "type" => "Announce" - } + data = + File.read!("test/fixtures/https__info.pleroma.site_activity.json") + |> Jason.decode!() - :error = Transmogrifier.handle_incoming(data) - end + Mock + |> expect(:call, fn + %{method: :get, url: "https://info.pleroma.site/activity.json"}, _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "https://framapiaf.org/users/admin/activities/1234", + "actor" => "https://framapiaf.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => "https://info.pleroma.site/activity.json", + "type" => "Announce" + } + + :error = Transmogrifier.handle_incoming(data) end end end diff --git a/test/federation/activity_pub/utils_test.exs b/test/federation/activity_pub/utils_test.exs index 5c471f3eb..35c81f07d 100644 --- a/test/federation/activity_pub/utils_test.exs +++ b/test/federation/activity_pub/utils_test.exs @@ -10,15 +10,11 @@ defmodule Mobilizon.Federation.ActivityPub.UtilsTest do alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Router.Helpers, as: Routes - setup_all do - HTTPoison.start() - end - describe "make" do test "comment data from struct" do comment = insert(:comment) tag = insert(:tag, title: "MyTag") - reply = insert(:comment, in_reply_to_comment: comment, tags: [tag]) + reply = insert(:comment, in_reply_to_comment: comment, tags: [tag], attributed_to: nil) assert %{ "type" => "Note", @@ -42,8 +38,8 @@ defmodule Mobilizon.Federation.ActivityPub.UtilsTest do end test "comment data from map" do - comment = insert(:comment) - reply = insert(:comment, in_reply_to_comment: comment) + comment = insert(:comment, attributed_to: nil) + reply = insert(:comment, in_reply_to_comment: comment, attributed_to: nil) to = ["https://www.w3.org/ns/activitystreams#Public"] comment_data = Converter.Comment.model_to_as(reply) assert comment_data["type"] == "Note" diff --git a/test/fixtures/geospatial/addok/geocode.json b/test/fixtures/geospatial/addok/geocode.json new file mode 100644 index 000000000..34cb8ab32 --- /dev/null +++ b/test/fixtures/geospatial/addok/geocode.json @@ -0,0 +1,31 @@ +{ + "type": "FeatureCollection", + "version": "draft", + "features": [ + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [4.842569, 45.751718] }, + "properties": { + "label": "10 Rue Jangot 69007 Lyon", + "score": 0.9999999999926557, + "housenumber": "10", + "id": "69387_3650_00010", + "type": "housenumber", + "x": 843232.29, + "y": 6518573.31, + "importance": 0.5454797306366062, + "name": "10 Rue Jangot", + "postcode": "69007", + "citycode": "69387", + "city": "Lyon", + "district": "Lyon 7e Arrondissement", + "context": "69, Rh\u00f4ne, Auvergne-Rh\u00f4ne-Alpes", + "street": "Rue Jangot", + "distance": 0 + } + } + ], + "attribution": "BAN", + "licence": "ETALAB-2.0", + "limit": 1 +} diff --git a/test/fixtures/geospatial/addok/search.json b/test/fixtures/geospatial/addok/search.json new file mode 100644 index 000000000..70cb287d7 --- /dev/null +++ b/test/fixtures/geospatial/addok/search.json @@ -0,0 +1,49 @@ +{ + "type": "FeatureCollection", + "version": "draft", + "features": [ + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [4.842569, 45.751718] }, + "properties": { + "label": "10 Rue Jangot 69007 Lyon", + "score": 0.8677708846033279, + "housenumber": "10", + "id": "69387_3650_00010", + "type": "housenumber", + "x": 843232.29, + "y": 6518573.31, + "importance": 0.5454797306366062, + "name": "10 Rue Jangot", + "postcode": "69007", + "citycode": "69387", + "city": "Lyon", + "district": "Lyon 7e Arrondissement", + "context": "69, Rh\u00f4ne, Auvergne-Rh\u00f4ne-Alpes", + "street": "Rue Jangot" + } + }, + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [2.440319, 50.371266] }, + "properties": { + "label": "Rue Jangon 62127 Bailleul-aux-Cornailles", + "score": 0.5269641371131077, + "id": "62070_0100", + "type": "street", + "x": 660129.18, + "y": 7030540.46, + "importance": 0.25814396978264664, + "name": "Rue Jangon", + "postcode": "62127", + "citycode": "62070", + "city": "Bailleul-aux-Cornailles", + "context": "62, Pas-de-Calais, Hauts-de-France" + } + } + ], + "attribution": "BAN", + "licence": "ETALAB-2.0", + "query": "10 Rue Jangot", + "limit": 5 +} diff --git a/test/fixtures/geospatial/google_maps/api_key_invalid.json b/test/fixtures/geospatial/google_maps/api_key_invalid.json new file mode 100644 index 000000000..6c0ff5888 --- /dev/null +++ b/test/fixtures/geospatial/google_maps/api_key_invalid.json @@ -0,0 +1,5 @@ +{ + "error_message": "The provided API key is invalid.", + "results": [], + "status": "REQUEST_DENIED" +} diff --git a/test/fixtures/geospatial/google_maps/geocode.json b/test/fixtures/geospatial/google_maps/geocode.json new file mode 100644 index 000000000..232d1885c --- /dev/null +++ b/test/fixtures/geospatial/google_maps/geocode.json @@ -0,0 +1,109 @@ +{ + "plus_code": { + "compound_code": "QR2V+M2 Lyon, France", + "global_code": "8FQ6QR2V+M2" + }, + "results": [ + { + "address_components": [ + { + "long_name": "10bis", + "short_name": "10bis", + "types": ["street_number"] + }, + { + "long_name": "Rue Jangot", + "short_name": "Rue Jangot", + "types": ["route"] + }, + { + "long_name": "Lyon", + "short_name": "Lyon", + "types": ["locality", "political"] + }, + { + "long_name": "Rhône", + "short_name": "Rhône", + "types": ["administrative_area_level_2", "political"] + }, + { + "long_name": "Auvergne-Rhône-Alpes", + "short_name": "Auvergne-Rhône-Alpes", + "types": ["administrative_area_level_1", "political"] + }, + { + "long_name": "France", + "short_name": "FR", + "types": ["country", "political"] + }, + { + "long_name": "69007", + "short_name": "69007", + "types": ["postal_code"] + } + ], + "formatted_address": "10bis Rue Jangot, 69007 Lyon, France", + "geometry": { + "location": { "lat": 45.751725, "lng": 4.8424966 }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { "lat": 45.7530739802915, "lng": 4.843845580291503 }, + "southwest": { "lat": 45.7503760197085, "lng": 4.841147619708499 } + } + }, + "place_id": "ChIJrW0QikTq9EcR96jk2OnO75w", + "plus_code": { + "compound_code": "QR2R+MX Lyon, France", + "global_code": "8FQ6QR2R+MX" + }, + "types": ["street_address"] + }, + { + "address_components": [ + { "long_name": "9", "short_name": "9", "types": ["street_number"] }, + { + "long_name": "Rue Jangot", + "short_name": "Rue Jangot", + "types": ["route"] + }, + { + "long_name": "Lyon", + "short_name": "Lyon", + "types": ["locality", "political"] + }, + { + "long_name": "Rhône", + "short_name": "Rhône", + "types": ["administrative_area_level_2", "political"] + }, + { + "long_name": "Auvergne-Rhône-Alpes", + "short_name": "Auvergne-Rhône-Alpes", + "types": ["administrative_area_level_1", "political"] + }, + { + "long_name": "France", + "short_name": "FR", + "types": ["country", "political"] + }, + { + "long_name": "69007", + "short_name": "69007", + "types": ["postal_code"] + } + ], + "formatted_address": "9 Rue Jangot, 69007 Lyon, France", + "geometry": { + "location": { "lat": 45.7518165, "lng": 4.8427168 }, + "location_type": "RANGE_INTERPOLATED", + "viewport": { + "northeast": { "lat": 45.7531654802915, "lng": 4.844065780291502 }, + "southwest": { "lat": 45.7504675197085, "lng": 4.841367819708497 } + } + }, + "place_id": "EiA5IFJ1ZSBKYW5nb3QsIDY5MDA3IEx5b24sIEZyYW5jZSIaEhgKFAoSCR8N2ItE6vRHEW9tyPnhQsUIEAk", + "types": ["street_address"] + } + ], + "status": "OK" +} diff --git a/test/fixtures/geospatial/google_maps/geocode_2.json b/test/fixtures/geospatial/google_maps/geocode_2.json new file mode 100644 index 000000000..9ab5debfa --- /dev/null +++ b/test/fixtures/geospatial/google_maps/geocode_2.json @@ -0,0 +1,62 @@ +{ + "html_attributions": [], + "result": { + "address_components": [ + { + "long_name": "10bis", + "short_name": "10bis", + "types": ["street_number"] + }, + { + "long_name": "Rue Jangot", + "short_name": "Rue Jangot", + "types": ["route"] + }, + { + "long_name": "Lyon", + "short_name": "Lyon", + "types": ["locality", "political"] + }, + { + "long_name": "Rhône", + "short_name": "Rhône", + "types": ["administrative_area_level_2", "political"] + }, + { + "long_name": "Auvergne-Rhône-Alpes", + "short_name": "Auvergne-Rhône-Alpes", + "types": ["administrative_area_level_1", "political"] + }, + { + "long_name": "France", + "short_name": "FR", + "types": ["country", "political"] + }, + { "long_name": "69007", "short_name": "69007", "types": ["postal_code"] } + ], + "adr_address": "\u003cspan class=\"street-address\"\u003e10bis Rue Jangot\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e69007\u003c/span\u003e \u003cspan class=\"locality\"\u003eLyon\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eFrance\u003c/span\u003e", + "formatted_address": "10bis Rue Jangot, 69007 Lyon, France", + "geometry": { + "location": { "lat": 45.751725, "lng": 4.8424966 }, + "viewport": { + "northeast": { "lat": 45.7531097802915, "lng": 4.843951380291502 }, + "southwest": { "lat": 45.7504118197085, "lng": 4.841253419708497 } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png", + "id": "4a3482a7a74c6203048adf713b736186c4ace7cd", + "name": "10bis Rue Jangot", + "place_id": "ChIJrW0QikTq9EcR96jk2OnO75w", + "plus_code": { + "compound_code": "QR2R+MX Lyon, France", + "global_code": "8FQ6QR2R+MX" + }, + "reference": "ChIJrW0QikTq9EcR96jk2OnO75w", + "scope": "GOOGLE", + "types": ["street_address"], + "url": "https://maps.google.com/?q=10bis+Rue+Jangot,+69007+Lyon,+France&ftid=0x47f4ea448a106dad:0x9cefcee9d8e4a8f7", + "utc_offset": 120, + "vicinity": "Lyon" + }, + "status": "OK" +} diff --git a/test/fixtures/geospatial/google_maps/search.json b/test/fixtures/geospatial/google_maps/search.json new file mode 100644 index 000000000..8d6c2f522 --- /dev/null +++ b/test/fixtures/geospatial/google_maps/search.json @@ -0,0 +1,55 @@ +{ + "results": [ + { + "address_components": [ + { "long_name": "10", "short_name": "10", "types": ["street_number"] }, + { + "long_name": "Rue Jangot", + "short_name": "Rue Jangot", + "types": ["route"] + }, + { + "long_name": "Lyon", + "short_name": "Lyon", + "types": ["locality", "political"] + }, + { + "long_name": "Rhône", + "short_name": "Rhône", + "types": ["administrative_area_level_2", "political"] + }, + { + "long_name": "Auvergne-Rhône-Alpes", + "short_name": "Auvergne-Rhône-Alpes", + "types": ["administrative_area_level_1", "political"] + }, + { + "long_name": "France", + "short_name": "FR", + "types": ["country", "political"] + }, + { + "long_name": "69007", + "short_name": "69007", + "types": ["postal_code"] + } + ], + "formatted_address": "10 Rue Jangot, 69007 Lyon, France", + "geometry": { + "location": { "lat": 45.75164940000001, "lng": 4.8424032 }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { "lat": 45.75299838029151, "lng": 4.843752180291502 }, + "southwest": { "lat": 45.75030041970851, "lng": 4.841054219708497 } + } + }, + "place_id": "ChIJtW0QikTq9EcRLI4Vy6bRx0U", + "plus_code": { + "compound_code": "QR2R+MX Lyon, France", + "global_code": "8FQ6QR2R+MX" + }, + "types": ["street_address"] + } + ], + "status": "OK" +} diff --git a/test/fixtures/geospatial/google_maps/search_2.json b/test/fixtures/geospatial/google_maps/search_2.json new file mode 100644 index 000000000..938567015 --- /dev/null +++ b/test/fixtures/geospatial/google_maps/search_2.json @@ -0,0 +1,58 @@ +{ + "html_attributions": [], + "result": { + "address_components": [ + { "long_name": "10", "short_name": "10", "types": ["street_number"] }, + { + "long_name": "Rue Jangot", + "short_name": "Rue Jangot", + "types": ["route"] + }, + { + "long_name": "Lyon", + "short_name": "Lyon", + "types": ["locality", "political"] + }, + { + "long_name": "Rhône", + "short_name": "Rhône", + "types": ["administrative_area_level_2", "political"] + }, + { + "long_name": "Auvergne-Rhône-Alpes", + "short_name": "Auvergne-Rhône-Alpes", + "types": ["administrative_area_level_1", "political"] + }, + { + "long_name": "France", + "short_name": "FR", + "types": ["country", "political"] + }, + { "long_name": "69007", "short_name": "69007", "types": ["postal_code"] } + ], + "adr_address": "\u003cspan class=\"street-address\"\u003e10 Rue Jangot\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e69007\u003c/span\u003e \u003cspan class=\"locality\"\u003eLyon\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eFrance\u003c/span\u003e", + "formatted_address": "10 Rue Jangot, 69007 Lyon, France", + "geometry": { + "location": { "lat": 45.75164940000001, "lng": 4.842403200000001 }, + "viewport": { + "northeast": { "lat": 45.7530412802915, "lng": 4.843668630291503 }, + "southwest": { "lat": 45.75034331970851, "lng": 4.840970669708498 } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png", + "id": "61b9418d092d2ed05ddd65a55dddefda5b9628cc", + "name": "10 Rue Jangot", + "place_id": "ChIJtW0QikTq9EcRLI4Vy6bRx0U", + "plus_code": { + "compound_code": "QR2R+MX Lyon, France", + "global_code": "8FQ6QR2R+MX" + }, + "reference": "ChIJtW0QikTq9EcRLI4Vy6bRx0U", + "scope": "GOOGLE", + "types": ["street_address"], + "url": "https://maps.google.com/?q=10+Rue+Jangot,+69007+Lyon,+France&ftid=0x47f4ea448a106db5:0x45c7d1a6cb158e2c", + "utc_offset": 120, + "vicinity": "Lyon" + }, + "status": "OK" +} diff --git a/test/fixtures/geospatial/map_quest/geocode.json b/test/fixtures/geospatial/map_quest/geocode.json new file mode 100644 index 000000000..822ccc93c --- /dev/null +++ b/test/fixtures/geospatial/map_quest/geocode.json @@ -0,0 +1,43 @@ +{ + "info": { + "statuscode": 0, + "copyright": { + "text": "\\u00A9 2019 MapQuest, Inc.", + "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif", + "imageAltText": "\\u00A9 2019 MapQuest, Inc." + }, + "messages": [] + }, + "options": { "maxResults": 1, "thumbMaps": true, "ignoreLatLngInput": false }, + "results": [ + { + "providedLocation": { "latLng": { "lat": 45.751718, "lng": 4.842569 } }, + "locations": [ + { + "street": "10 Rue Jangot", + "adminArea6": "", + "adminArea6Type": "Neighborhood", + "adminArea5": "Lyon", + "adminArea5Type": "City", + "adminArea4": "", + "adminArea4Type": "County", + "adminArea3": "Auvergne-Rhône-Alpes", + "adminArea3Type": "State", + "adminArea1": "FR", + "adminArea1Type": "Country", + "postalCode": "69007", + "geocodeQualityCode": "P1AAA", + "geocodeQuality": "POINT", + "dragPoint": false, + "sideOfStreet": "N", + "linkId": "0", + "unknownInput": "", + "type": "s", + "latLng": { "lat": 45.751714, "lng": 4.842566 }, + "displayLatLng": { "lat": 45.751714, "lng": 4.842566 }, + "mapUrl": "http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=-570915433" + } + ] + } + ] +} diff --git a/test/fixtures/geospatial/map_quest/search.json b/test/fixtures/geospatial/map_quest/search.json new file mode 100644 index 000000000..d58195ee9 --- /dev/null +++ b/test/fixtures/geospatial/map_quest/search.json @@ -0,0 +1,47 @@ +{ + "info": { + "statuscode": 0, + "copyright": { + "text": "\\u00A9 2019 MapQuest, Inc.", + "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif", + "imageAltText": "\\u00A9 2019 MapQuest, Inc." + }, + "messages": [] + }, + "options": { + "maxResults": 10, + "thumbMaps": true, + "ignoreLatLngInput": false + }, + "results": [ + { + "providedLocation": { "location": "10 rue Jangot" }, + "locations": [ + { + "street": "10 Rue Jangot", + "adminArea6": "7e", + "adminArea6Type": "Neighborhood", + "adminArea5": "Lyon", + "adminArea5Type": "City", + "adminArea4": "Lyon", + "adminArea4Type": "County", + "adminArea3": "Auvergne-Rhône-Alpes", + "adminArea3Type": "State", + "adminArea1": "FR", + "adminArea1Type": "Country", + "postalCode": "69007", + "geocodeQualityCode": "P1AXX", + "geocodeQuality": "POINT", + "dragPoint": false, + "sideOfStreet": "N", + "linkId": "0", + "unknownInput": "", + "type": "s", + "latLng": { "lat": 45.751714, "lng": 4.842566 }, + "displayLatLng": { "lat": 45.751714, "lng": 4.842566 }, + "mapUrl": "http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=1358091752" + } + ] + } + ] +} diff --git a/test/fixtures/geospatial/nominatim/geocode.json b/test/fixtures/geospatial/nominatim/geocode.json new file mode 100644 index 000000000..ed712f68c --- /dev/null +++ b/test/fixtures/geospatial/nominatim/geocode.json @@ -0,0 +1,43 @@ +{ + "type": "FeatureCollection", + "geocoding": { + "version": "0.1.0", + "attribution": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", + "licence": "ODbL", + "query": "45.751718,4.842569" + }, + "features": [ + { + "type": "Feature", + "properties": { + "geocoding": { + "place_id": 41453794, + "osm_type": "node", + "osm_id": 3078260611, + "type": "house", + "accuracy": 0, + "label": "10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France", + "name": null, + "housenumber": "10", + "street": "Rue Jangot", + "postcode": "69007", + "city": "Lyon", + "county": "Lyon", + "state": "Auvergne-Rhône-Alpes", + "country": "France", + "admin": { + "level2": "France", + "level3": "Metropolitan France", + "level4": "Auvergne-Rhône-Alpes", + "level5": "Departemental constituency of Rhône", + "level6": "Métropole de Lyon", + "level7": "Lyon", + "level8": "Lyon", + "level9": "Lyon 7e Arrondissement" + } + } + }, + "geometry": { "type": "Point", "coordinates": [4.8425657, 45.7517141] } + } + ] +} diff --git a/test/fixtures/geospatial/nominatim/search.json b/test/fixtures/geospatial/nominatim/search.json new file mode 100644 index 000000000..e03324fa2 --- /dev/null +++ b/test/fixtures/geospatial/nominatim/search.json @@ -0,0 +1,42 @@ +{ + "type": "FeatureCollection", + "geocoding": { + "version": "0.1.0", + "attribution": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", + "licence": "ODbL", + "query": "10 rue Jangot" + }, + "features": [ + { + "type": "Feature", + "properties": { + "geocoding": { + "place_id": 41453794, + "osm_type": "node", + "osm_id": 3078260611, + "type": "house", + "label": "10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France", + "name": null, + "housenumber": "10", + "street": "Rue Jangot", + "postcode": "69007", + "city": "Lyon", + "county": "Lyon", + "state": "Auvergne-Rhône-Alpes", + "country": "France", + "admin": { + "level2": "France", + "level3": "Metropolitan France", + "level4": "Auvergne-Rhône-Alpes", + "level5": "Departemental constituency of Rhône", + "level6": "Métropole de Lyon", + "level7": "Lyon", + "level8": "Lyon", + "level9": "Lyon 7e Arrondissement" + } + } + }, + "geometry": { "type": "Point", "coordinates": [4.8425657, 45.7517141] } + } + ] +} diff --git a/test/fixtures/geospatial/photon/search.json b/test/fixtures/geospatial/photon/search.json new file mode 100644 index 000000000..7a775eaa7 --- /dev/null +++ b/test/fixtures/geospatial/photon/search.json @@ -0,0 +1,37 @@ +{ + "features": [ + { + "geometry": { "coordinates": [4.8425657, 45.7517141], "type": "Point" }, + "type": "Feature", + "properties": { + "osm_id": 3078260611, + "osm_type": "N", + "country": "France", + "osm_key": "place", + "housenumber": "10", + "city": "Lyon", + "street": "Rue Jangot", + "osm_value": "house", + "postcode": "69007", + "state": "Auvergne-Rhône-Alpes" + } + }, + { + "geometry": { "coordinates": [4.8424254, 45.7517056], "type": "Point" }, + "type": "Feature", + "properties": { + "osm_id": 3078260612, + "osm_type": "N", + "country": "France", + "osm_key": "place", + "housenumber": "10bis", + "city": "Lyon", + "street": "Rue Jangot", + "osm_value": "house", + "postcode": "69007", + "state": "Auvergne-Rhône-Alpes" + } + } + ], + "type": "FeatureCollection" +} diff --git a/test/fixtures/https__info.pleroma.site_activity.json b/test/fixtures/https__info.pleroma.site_activity.json new file mode 100644 index 000000000..ae18eb7b0 --- /dev/null +++ b/test/fixtures/https__info.pleroma.site_activity.json @@ -0,0 +1,12 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "http://mastodon.example.org/users/admin", + "attachment": [], + "attributedTo": "http://mastodon.example.org/users/admin", + "content": "

    this post was not actually written by Haelwenn

    ", + "id": "https://info.pleroma.site/activity.json", + "published": "2018-09-01T22:15:00Z", + "tag": [], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Note" +} diff --git a/test/fixtures/mastodon-actor.json b/test/fixtures/mastodon-actor.json new file mode 100644 index 000000000..70d41e926 --- /dev/null +++ b/test/fixtures/mastodon-actor.json @@ -0,0 +1,91 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "IdentityProof": "toot:IdentityProof", + "discoverable": "toot:discoverable", + "Device": "toot:Device", + "Ed25519Signature": "toot:Ed25519Signature", + "Ed25519Key": "toot:Ed25519Key", + "Curve25519Key": "toot:Curve25519Key", + "EncryptedMessage": "toot:EncryptedMessage", + "publicKeyBase64": "toot:publicKeyBase64", + "deviceId": "toot:deviceId", + "claim": { + "@type": "@id", + "@id": "toot:claim" + }, + "fingerprintKey": { + "@type": "@id", + "@id": "toot:fingerprintKey" + }, + "identityKey": { + "@type": "@id", + "@id": "toot:identityKey" + }, + "devices": { + "@type": "@id", + "@id": "toot:devices" + }, + "messageFranking": "toot:messageFranking", + "messageType": "toot:messageType", + "cipherText": "toot:cipherText", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://framapiaf.org/users/peertube", + "type": "Person", + "following": "https://framapiaf.org/users/peertube/following", + "followers": "https://framapiaf.org/users/peertube/followers", + "inbox": "https://framapiaf.org/users/peertube/inbox", + "outbox": "https://framapiaf.org/users/peertube/outbox", + "featured": "https://framapiaf.org/users/peertube/collections/featured", + "preferredUsername": "peertube", + "name": "PeerTube", + "summary": "

    PeerTube Software Official Account - No support here // Compte officiel du logiciel PeerTube (animé par Framasoft). Nous ne faisons pas de support depuis ce compte. Merci de contacter l'administrateur⋅ice de l'instance concernée ou de vous rendre sur https://framacolibri.org/c/peertube

    ", + "url": "https://framapiaf.org/@peertube", + "manuallyApprovesFollowers": false, + "discoverable": false, + "devices": "https://framapiaf.org/users/peertube/collections/devices", + "publicKey": { + "id": "https://framapiaf.org/users/peertube#main-key", + "owner": "https://framapiaf.org/users/peertube", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn1y53r+ymOmDoP8iYxa1\nb1VvXkldZVpxJg1ZVeq4SijVS3oNurrQQhpTwTmCCAue2m+UvG4eEEYAYSfb5+C3\nbqH3kLlQrptkp8y/qz3d4tk/b8RConAaws7/SwksDC5rs+cYLnnXgD7rAaT1uH/B\nVTzG79YLgnasK6IxpnBth6Vru+9g2U8PzIUOfuwPV3aZeu9q2xEdC5/GnnjsfKZv\nWEzpG3HkRAlaTRDYadl9dWOPlfhy/LMkknAP02j+Qt/s7y83YqsrUyvQcfTSy3Zf\nLNNFrpU4u1ACyZXzvaoDXQH8HetKSA06xqa4pJO4xmM2PWMoBq1KX3Us4sP291w4\nEQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "tag": [], + "attachment": [], + "endpoints": { + "sharedInbox": "https://framapiaf.org/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/223/824/original/03ed95406a9a3cd0.png" + }, + "image": { + "type": "Image", + "mediaType": "image/png", + "url": "https://framapiaf.s3.framasoft.org/framapiaf/accounts/headers/000/223/824/original/2fbb4d6268c2fb20.png" + } +} diff --git a/test/fixtures/mastodon-announce.json b/test/fixtures/mastodon-announce.json index 83da31bbc..16c2bb178 100644 --- a/test/fixtures/mastodon-announce.json +++ b/test/fixtures/mastodon-announce.json @@ -1,8 +1,6 @@ { "type": "Announce", - "to": [ - "https://www.w3.org/ns/activitystreams#Public" - ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], "signature": { "type": "RsaSignature2017", "signatureValue": "T95DRE0eAligvMuRMkQA01lsoz2PKi4XXF+cyZ0BqbrO12p751TEWTyyRn5a+HH0e4kc77EUhQVXwMq80WAYDzHKVUTf2XBJPBa68vl0j6RXw3+HK4ef5hR4KWFNBU34yePS7S1fEmc1mTG4Yx926wtmZwDpEMTp1CXOeVEjCYzmdyHpepPPH2ZZettiacmPRSqBLPGWZoot7kH/SioIdnrMGY0I7b+rqkIdnnEcdhu9N1BKPEO9Sr+KmxgAUiidmNZlbBXX6gCxp8BiIdH4ABsIcwoDcGNkM5EmWunGW31LVjsEQXhH5c1Wly0ugYYPCg/0eHLNBOhKkY/teSM8Lg==", @@ -10,14 +8,14 @@ "created": "2018-02-17T19:39:15Z" }, "published": "2018-02-17T19:39:15Z", - "object": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400", - "id": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity", + "object": "https://framapiaf.org/users/peertube/statuses/104584600044284729", + "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729/activity", "cc": [ - "https://framapiaf.org/users/Framasoft", - "https://framapiaf.org/users/Framasoft/followers" + "https://framapiaf.org/users/peertube", + "https://framapiaf.org/users/peertube/followers" ], - "atomUri": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity", - "actor": "https://framapiaf.org/users/Framasoft", + "atomUri": "https://framapiaf.org/users/peertube/statuses/104584600044284729/activity", + "actor": "https://framapiaf.org/users/peertube", "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", diff --git a/test/fixtures/mastodon-status-2.json b/test/fixtures/mastodon-status-2.json new file mode 100644 index 000000000..fe58c7fab --- /dev/null +++ b/test/fixtures/mastodon-status-2.json @@ -0,0 +1,68 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "blurhash": "toot:blurhash", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + }, + "Hashtag": "as:Hashtag" + } + ], + "id": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2019-05-14T09:13:13Z", + "url": "https://framapiaf.org/@Framasoft/102093631881522097", + "attributedTo": "https://framapiaf.org/users/Framasoft", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://framapiaf.org/users/Framasoft/followers"], + "sensitive": false, + "atomUri": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097", + "inReplyToAtomUri": null, + "conversation": "tag:framapiaf.org,2019-05-14:objectId=16200311:objectType=Conversation", + "content": "

    Mobilizon : Finançons un outil pour sortir nos événements de Facebook !

    https://framablog.org/2019/05/14/mobilizon-financons-un-outil-pour-sortir-nos-evenements-de-facebook/ #Framablog #TootOuRien

    Nous avons moins de 60 jours pour financer Mobilizon. Moins de 60 jours pour faire connaître notre projet d'alternative libre et fédérée aux événements Facebook ; et pour savoir à quel point nous devons nous y investir.

    Changer le logiciel de celles et ceux qui changent le …

    ", + "contentMap": { + "fr": "

    Mobilizon : Finançons un outil pour sortir nos événements de Facebook !

    https://framablog.org/2019/05/14/mobilizon-financons-un-outil-pour-sortir-nos-evenements-de-facebook/ #Framablog #TootOuRien

    Nous avons moins de 60 jours pour financer Mobilizon. Moins de 60 jours pour faire connaître notre projet d'alternative libre et fédérée aux événements Facebook ; et pour savoir à quel point nous devons nous y investir.

    Changer le logiciel de celles et ceux qui changent le …

    " + }, + "attachment": [ + { + "type": "Document", + "mediaType": "image/jpeg", + "url": "https://framapiaf.s3.framasoft.org/framapiaf/media_attachments/files/003/337/144/original/39d457fd9e0f0171.jpg", + "name": null, + "blurhash": "UIF=jrpIM|~q~VT0%2t6Ne9a?G-;9ZRP%2Rk" + } + ], + "tag": [ + { + "type": "Hashtag", + "href": "https://framapiaf.org/tags/framablog", + "name": "#framablog" + }, + { + "type": "Hashtag", + "href": "https://framapiaf.org/tags/tootourien", + "name": "#tootourien" + } + ], + "replies": { + "id": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097/replies?only_other_accounts=true&page=true", + "partOf": "https://framapiaf.org/users/Framasoft/statuses/102093631881522097/replies", + "items": [] + } + } +} diff --git a/test/fixtures/mastodon-status-3.json b/test/fixtures/mastodon-status-3.json new file mode 100644 index 000000000..a650d063d --- /dev/null +++ b/test/fixtures/mastodon-status-3.json @@ -0,0 +1,34 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://zoltasila.pl/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "actor": "https://zoltasila.pl/users/mkljczk", + "attachment": [], + "attributedTo": "https://zoltasila.pl/users/mkljczk", + "cc": ["https://zoltasila.pl/users/mkljczk/followers"], + "content": "

    @peertube guess you wanted to put the en_US lang link

    ", + "context": "tag:framapiaf.org,2020-07-27:objectId=39135637:objectType=Conversation", + "conversation": "tag:framapiaf.org,2020-07-27:objectId=39135637:objectType=Conversation", + "id": "https://zoltasila.pl/objects/1c295713-8e3c-411e-9e62-57a7b9c9e514", + "inReplyTo": "https://framapiaf.org/users/peertube/statuses/104584600044284729", + "published": "2020-07-27T09:37:57.202806Z", + "sensitive": false, + "source": "@peertube@framapiaf.org guess you wanted to put the [en_US lang link](https://joinpeertube.org/en_US/news#release-2-3-0)", + "summary": "", + "tag": [ + { + "href": "https://framapiaf.org/users/peertube", + "name": "@peertube@framapiaf.org", + "type": "Mention" + } + ], + "to": [ + "https://framapiaf.org/users/peertube", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Note" +} diff --git a/test/fixtures/mastodon-status-4.json b/test/fixtures/mastodon-status-4.json new file mode 100644 index 000000000..31e70078f --- /dev/null +++ b/test/fixtures/mastodon-status-4.json @@ -0,0 +1,56 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "blurhash": "toot:blurhash", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2020-07-27T07:19:11Z", + "url": "https://framapiaf.org/@peertube/104584600044284729", + "attributedTo": "https://framapiaf.org/users/peertube", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://framapiaf.org/users/peertube/followers"], + "sensitive": false, + "atomUri": "https://framapiaf.org/users/peertube/statuses/104584600044284729", + "inReplyToAtomUri": null, + "conversation": "tag:framapiaf.org,2020-07-27:objectId=39135637:objectType=Conversation", + "content": "

    PeerTube 2.3 is out! Discover on https://joinpeertube.org/fr_FR/news#release-2-3-0 the list of new features!

    Have you seen the broadcast message system ? 🤩

    ", + "contentMap": { + "en": "

    PeerTube 2.3 is out! Discover on https://joinpeertube.org/fr_FR/news#release-2-3-0 the list of new features!

    Have you seen the broadcast message system ? 🤩

    " + }, + "attachment": [ + { + "type": "Document", + "mediaType": "image/png", + "url": "https://framapiaf.s3.framasoft.org/framapiaf/media_attachments/files/104/584/599/807/860/387/original/88c94143f78fdfa3.png", + "name": null, + "blurhash": "U5SY?Z00nOxu7ORP.8-pU^kVS#NGXyxbMxM{" + } + ], + "tag": [], + "replies": { + "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies?only_other_accounts=true&page=true", + "partOf": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies", + "items": [] + } + } +} diff --git a/test/fixtures/mastodon-status-5.json b/test/fixtures/mastodon-status-5.json new file mode 100644 index 000000000..ff19c6134 --- /dev/null +++ b/test/fixtures/mastodon-status-5.json @@ -0,0 +1,58 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount" + } + ], + "id": "https://diaspodon.fr/users/dada/statuses/100820008426311925", + "type": "Note", + "summary": null, + "inReplyTo": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d", + "published": "2018-10-01T10:54:01Z", + "url": "https://diaspodon.fr/@dada/100820008426311925", + "attributedTo": "https://diaspodon.fr/users/dada", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": [ + "https://diaspodon.fr/users/dada/followers", + "https://framapiaf.org/users/Pouhiou", + "https://framatube.org/accounts/framasoft" + ], + "sensitive": false, + "atomUri": "https://diaspodon.fr/users/dada/statuses/100820008426311925", + "inReplyToAtomUri": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d", + "conversation": "tag:diaspodon.fr,2018-10-01:objectId=1187066:objectType=Conversation", + "content": "

    @framasoft Ça ne serait pas la voix de @Pouhiou ? 🤔

    ", + "contentMap": { + "fr": "

    @framasoft Ça ne serait pas la voix de @Pouhiou ? 🤔

    " + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "https://framatube.org/accounts/framasoft", + "name": "@framasoft@framatube.org" + }, + { + "type": "Mention", + "href": "https://framapiaf.org/users/Pouhiou", + "name": "@Pouhiou@framapiaf.org" + } + ], + "replies": { + "id": "https://diaspodon.fr/users/dada/statuses/100820008426311925/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://diaspodon.fr/users/dada/statuses/100820008426311925/replies?only_other_accounts=true&page=true", + "partOf": "https://diaspodon.fr/users/dada/statuses/100820008426311925/replies", + "items": [] + } + } +} diff --git a/test/fixtures/mastodon-status.json b/test/fixtures/mastodon-status.json new file mode 100644 index 000000000..31e70078f --- /dev/null +++ b/test/fixtures/mastodon-status.json @@ -0,0 +1,56 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "blurhash": "toot:blurhash", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2020-07-27T07:19:11Z", + "url": "https://framapiaf.org/@peertube/104584600044284729", + "attributedTo": "https://framapiaf.org/users/peertube", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://framapiaf.org/users/peertube/followers"], + "sensitive": false, + "atomUri": "https://framapiaf.org/users/peertube/statuses/104584600044284729", + "inReplyToAtomUri": null, + "conversation": "tag:framapiaf.org,2020-07-27:objectId=39135637:objectType=Conversation", + "content": "

    PeerTube 2.3 is out! Discover on https://joinpeertube.org/fr_FR/news#release-2-3-0 the list of new features!

    Have you seen the broadcast message system ? 🤩

    ", + "contentMap": { + "en": "

    PeerTube 2.3 is out! Discover on https://joinpeertube.org/fr_FR/news#release-2-3-0 the list of new features!

    Have you seen the broadcast message system ? 🤩

    " + }, + "attachment": [ + { + "type": "Document", + "mediaType": "image/png", + "url": "https://framapiaf.s3.framasoft.org/framapiaf/media_attachments/files/104/584/599/807/860/387/original/88c94143f78fdfa3.png", + "name": null, + "blurhash": "U5SY?Z00nOxu7ORP.8-pU^kVS#NGXyxbMxM{" + } + ], + "tag": [], + "replies": { + "id": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies?only_other_accounts=true&page=true", + "partOf": "https://framapiaf.org/users/peertube/statuses/104584600044284729/replies", + "items": [] + } + } +} diff --git a/test/fixtures/mobilizon-post-activity-group.json b/test/fixtures/mobilizon-post-activity-group.json new file mode 100644 index 000000000..35444b402 --- /dev/null +++ b/test/fixtures/mobilizon-post-activity-group.json @@ -0,0 +1,52 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://litepub.social/litepub/context.jsonld", + { + "Hashtag": "as:Hashtag", + "category": "sc:category", + "ical": "http://www.w3.org/2002/12/cal/ical#", + "joinMode": { + "@id": "mz:joinMode", + "@type": "mz:joinModeType" + }, + "joinModeType": { + "@id": "mz:joinModeType", + "@type": "rdfs:Class" + }, + "maximumAttendeeCapacity": "sc:maximumAttendeeCapacity", + "mz": "https://joinmobilizon.org/ns#", + "repliesModerationOption": { + "@id": "mz:repliesModerationOption", + "@type": "mz:repliesModerationOptionType" + }, + "repliesModerationOptionType": { + "@id": "mz:repliesModerationOptionType", + "@type": "rdfs:Class" + }, + "sc": "http://schema.org#", + "uuid": "sc:identifier" + } + ], + "actor": "http://mobilizon1.com/@user", + "attributedTo": "http://mobilizon1.com/@group", + "cc": [], + "id": "http://mobilizon1.com/events/f270ae07-7991-453c-9bb7-3d2122ededae/activity", + "object": { + "actor": "http://mobilizon1.com/@user", + "attributedTo": "http://mobilizon1.com/@group", + "startTime": "2018-02-12T14:08:20Z", + "cc": [], + "content": "

    @tcit

    ", + "id": "http://mobilizon1.com/events/f270ae07-7991-453c-9bb7-3d2122ededae", + "name": "My first event", + "published": "2018-02-12T14:08:20Z", + "tag": [], + "to": ["http://mobilizon1.com/@group"], + "type": "Event", + "uuid": "f270ae07-7991-453c-9bb7-3d2122ededae" + }, + "published": "2018-02-12T14:08:20Z", + "to": ["http://mobilizon1.com/@group"], + "type": "Create" +} diff --git a/test/fixtures/peertube-video.json b/test/fixtures/peertube-video.json new file mode 100644 index 000000000..fb36f2c59 --- /dev/null +++ b/test/fixtures/peertube-video.json @@ -0,0 +1,393 @@ +{ + "type": "Video", + "id": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d", + "name": "What is PeerTube?", + "duration": "PT113S", + "uuid": "9c9de5e8-0a1e-484a-b099-e80766180a6d", + "tag": [ + { + "type": "Hashtag", + "name": "framasoft" + }, + { + "type": "Hashtag", + "name": "peertube" + } + ], + "category": { + "identifier": "15", + "name": "Science & Technology" + }, + "licence": { + "identifier": "2", + "name": "Attribution - Share Alike" + }, + "language": { + "identifier": "en", + "name": "English" + }, + "views": 53137, + "sensitive": false, + "waitTranscoding": true, + "state": 1, + "commentsEnabled": true, + "downloadEnabled": true, + "published": "2018-10-01T10:52:46.396Z", + "originallyPublishedAt": null, + "updated": "2020-07-30T08:01:00.836Z", + "mediaType": "text/markdown", + "content": "**[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n**Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)**\r\n*A decentralized video hosting network, based on free/libre software!*\r\n\r\n**Animation Produced by:** [LILA](https://libreart.info) - [ZeMarmot Team](https://film.zemarmot.net)\r\n*Directed by* Aryeom\r\n*Assistant* Jehan\r\n**Licence**: [CC-By-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\r\n\r\n**Sponsored by** [Framasoft](https://framasoft.org)\r\n\r\n**Music**: [Red Step Forward](http://play.dogmazic.net/song.php?song_id=52491) - CC-By Ken Bushima\r\n\r\n**Movie Clip**: [Caminades 3: Llamigos](http://www.caminandes.com/) CC-By Blender Institute\r\n\r\n**Video sources**: https://gitlab.gnome.org/Jehan/what-is-peertube/", + "support": null, + "subtitleLanguage": [ + { + "identifier": "ca", + "name": "Catalan", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-ca.vtt" + }, + { + "identifier": "cs", + "name": "Czech", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-cs.vtt" + }, + { + "identifier": "de", + "name": "German", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-de.vtt" + }, + { + "identifier": "en", + "name": "English", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-en.vtt" + }, + { + "identifier": "es", + "name": "Spanish", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-es.vtt" + }, + { + "identifier": "eu", + "name": "Basque", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-eu.vtt" + }, + { + "identifier": "fr", + "name": "French", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-fr.vtt" + }, + { + "identifier": "gl", + "name": "Galician", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-gl.vtt" + }, + { + "identifier": "hu", + "name": "Hungarian", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-hu.vtt" + }, + { + "identifier": "it", + "name": "Italian", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-it.vtt" + }, + { + "identifier": "lt", + "name": "Lithuanian", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-lt.vtt" + }, + { + "identifier": "nl", + "name": "Dutch", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-nl.vtt" + }, + { + "identifier": "pl", + "name": "Polish", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-pl.vtt" + }, + { + "identifier": "pt", + "name": "Portuguese", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-pt.vtt" + }, + { + "identifier": "ru", + "name": "Russian", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-ru.vtt" + }, + { + "identifier": "sv", + "name": "Swedish", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-sv.vtt" + }, + { + "identifier": "zh", + "name": "Chinese", + "url": "https://framatube.org/static/video-captions/9c9de5e8-0a1e-484a-b099-e80766180a6d-zh.vtt" + } + ], + "icon": [ + { + "type": "Image", + "url": "https://framatube.org/static/thumbnails/9c9de5e8-0a1e-484a-b099-e80766180a6d.jpg", + "mediaType": "image/jpeg", + "width": 223, + "height": 122 + }, + { + "type": "Image", + "url": "https://framatube.org/static/previews/9c9de5e8-0a1e-484a-b099-e80766180a6d.jpg", + "mediaType": "image/jpeg", + "width": 850, + "height": 480 + } + ], + "url": [ + { + "type": "Link", + "mediaType": "text/html", + "href": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d" + }, + { + "type": "Link", + "mediaType": "video/mp4", + "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4", + "height": 1080, + "size": 14689568, + "fps": 24 + }, + { + "type": "Link", + "rel": ["metadata", "video/mp4"], + "mediaType": "application/json", + "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10124", + "height": 1080, + "fps": 24 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent", + "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.torrent", + "height": 1080 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", + "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.torrent&xt=urn:btih:dc84b692c4002fec0cae873df0dc7f5d67fc09db&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-1080.mp4", + "height": 1080 + }, + { + "type": "Link", + "mediaType": "video/mp4", + "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4", + "height": 720, + "size": 8365049, + "fps": 24 + }, + { + "type": "Link", + "rel": ["metadata", "video/mp4"], + "mediaType": "application/json", + "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10127", + "height": 720, + "fps": 24 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent", + "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-720.torrent", + "height": 720 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", + "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.torrent&xt=urn:btih:9fb0e35b4945565fa842b2bb0fe8b03edf223b15&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-720.mp4", + "height": 720 + }, + { + "type": "Link", + "mediaType": "video/mp4", + "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4", + "height": 480, + "size": 5650553, + "fps": 24 + }, + { + "type": "Link", + "rel": ["metadata", "video/mp4"], + "mediaType": "application/json", + "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10125", + "height": 480, + "fps": 24 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent", + "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-480.torrent", + "height": 480 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", + "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.torrent&xt=urn:btih:bf02027a9aad4275d8baa25afae230b85187bcf7&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-480.mp4", + "height": 480 + }, + { + "type": "Link", + "mediaType": "video/mp4", + "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4", + "height": 360, + "size": 4524751, + "fps": 24 + }, + { + "type": "Link", + "rel": ["metadata", "video/mp4"], + "mediaType": "application/json", + "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10126", + "height": 360, + "fps": 24 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent", + "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-360.torrent", + "height": 360 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", + "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.torrent&xt=urn:btih:16b3720b18752523e2848341d3207120f2de26f8&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-360.mp4", + "height": 360 + }, + { + "type": "Link", + "mediaType": "video/mp4", + "href": "https://framatube.org/static/webseed/9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4", + "height": 240, + "size": 3452697, + "fps": 24 + }, + { + "type": "Link", + "rel": ["metadata", "video/mp4"], + "mediaType": "application/json", + "href": "https://framatube.org/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/metadata/10128", + "height": 240, + "fps": 24 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent", + "href": "https://framatube.org/static/torrents/9c9de5e8-0a1e-484a-b099-e80766180a6d-240.torrent", + "height": 240 + }, + { + "type": "Link", + "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", + "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.torrent&xt=urn:btih:38b4747ff788b30bf61f59d1965cd38f9e48e01f&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fvideo.antopie.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.social%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.foxfam.club%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.freeforge.eu%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.krapace.fr%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fvideo.blueline.mg%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.video%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Ftube.crapaud-fou.org%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fflim.ml%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4&ws=https%3A%2F%2Fpeertube.nomagic.uk%2Fstatic%2Fredundancy%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4", + "height": 240 + } + ], + "likes": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d/likes", + "dislikes": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d/dislikes", + "shares": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d/announces", + "comments": "https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d/comments", + "attributedTo": [ + { + "type": "Person", + "id": "https://framatube.org/accounts/framasoft" + }, + { + "type": "Group", + "id": "https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://framatube.org/accounts/framasoft/followers"], + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "RsaSignature2017": "https://w3id.org/security#RsaSignature2017" + }, + { + "pt": "https://joinpeertube.org/ns#", + "sc": "http://schema.org#", + "Hashtag": "as:Hashtag", + "uuid": "sc:identifier", + "category": "sc:category", + "licence": "sc:license", + "subtitleLanguage": "sc:subtitleLanguage", + "sensitive": "as:sensitive", + "language": "sc:inLanguage", + "Infohash": "pt:Infohash", + "Playlist": "pt:Playlist", + "PlaylistElement": "pt:PlaylistElement", + "originallyPublishedAt": "sc:datePublished", + "views": { + "@type": "sc:Number", + "@id": "pt:views" + }, + "state": { + "@type": "sc:Number", + "@id": "pt:state" + }, + "size": { + "@type": "sc:Number", + "@id": "pt:size" + }, + "fps": { + "@type": "sc:Number", + "@id": "pt:fps" + }, + "startTimestamp": { + "@type": "sc:Number", + "@id": "pt:startTimestamp" + }, + "stopTimestamp": { + "@type": "sc:Number", + "@id": "pt:stopTimestamp" + }, + "position": { + "@type": "sc:Number", + "@id": "pt:position" + }, + "commentsEnabled": { + "@type": "sc:Boolean", + "@id": "pt:commentsEnabled" + }, + "downloadEnabled": { + "@type": "sc:Boolean", + "@id": "pt:downloadEnabled" + }, + "waitTranscoding": { + "@type": "sc:Boolean", + "@id": "pt:waitTranscoding" + }, + "support": { + "@type": "sc:Text", + "@id": "pt:support" + }, + "likes": { + "@id": "as:likes", + "@type": "@id" + }, + "dislikes": { + "@id": "as:dislikes", + "@type": "@id" + }, + "playlists": { + "@id": "pt:playlists", + "@type": "@id" + }, + "shares": { + "@id": "as:shares", + "@type": "@id" + }, + "comments": { + "@id": "as:comments", + "@type": "@id" + } + } + ] +} diff --git a/test/fixtures/pleroma-comment-object.json b/test/fixtures/pleroma-comment-object.json new file mode 100644 index 000000000..843170f5f --- /dev/null +++ b/test/fixtures/pleroma-comment-object.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://blob.cat/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "actor": "https://blob.cat/users/comicbot", + "attachment": [ + { + "mediaType": "image/gif", + "name": "1574936800141.gif", + "type": "Document", + "url": "https://blob.cat/media/143ba9b1ed15e67d7401906f7b71a459b90680af7075af5b8ac9cb8e3b86868a.gif" + } + ], + "attributedTo": "https://blob.cat/users/comicbot", + "cc": ["https://blob.cat/users/comicbot/followers"], + "content": "Super Mega Comics
    http://supermegacomics.com/", + "context": "https://blob.cat/contexts/26f3271a-3eb8-4f3f-8fb1-8ff96e2c4a47", + "conversation": "https://blob.cat/contexts/26f3271a-3eb8-4f3f-8fb1-8ff96e2c4a47", + "id": "https://blob.cat/objects/02fdea3d-932c-4348-9ecb-3f9eb3fbdd94", + "published": "2019-11-28T10:26:42.503473Z", + "sensitive": false, + "summary": "", + "tag": [], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Note" +} diff --git a/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json b/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json deleted file mode 100644 index 0c696bf27..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": { - "Accept": "*/*", - "Accept-Encoding":"deflate, gzip" - }, - "method": "get", - "options": [], - "request_body": "", - "url": "https://api-adresse.data.gouv.fr/reverse/?lon=4.842569&lat=45.751718" - }, - "response": { - "binary": false, - "body": "{\"type\": \"FeatureCollection\", \"version\": \"draft\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [4.842569, 45.751718]}, \"properties\": {\"label\": \"10 Rue Jangot 69007 Lyon\", \"score\": 0.9999999999926557, \"housenumber\": \"10\", \"id\": \"69387_3650_00010\", \"type\": \"housenumber\", \"x\": 843232.29, \"y\": 6518573.31, \"importance\": 0.5454797306366062, \"name\": \"10 Rue Jangot\", \"postcode\": \"69007\", \"citycode\": \"69387\", \"city\": \"Lyon\", \"district\": \"Lyon 7e Arrondissement\", \"context\": \"69, Rh\u00f4ne, Auvergne-Rh\u00f4ne-Alpes\", \"street\": \"Rue Jangot\", \"distance\": 0}}], \"attribution\": \"BAN\", \"licence\": \"ETALAB-2.0\", \"limit\": 1}", - "headers": { - "Server": "nginx/1.9.3", - "Date": "Thu, 14 Mar 2019 10:46:45 GMT", - "Content-type":"application/json; charset=utf-8", - "Vary":"Accept-Encoding", - "X-cache-status":"MISS", - "Access-control-allow-origin":"*", - "Access-control-allow-headers":"X-Requested-With,Content-Type", - "Content-encoding":"gzip" - }, - "status_code": 200, - "type": "ok" - } - } -] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/geospatial/addok/search.json b/test/fixtures/vcr_cassettes/geospatial/addok/search.json deleted file mode 100644 index d471d695d..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/addok/search.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": { - "Accept": "*/*", - "Accept-Encoding":"deflate, gzip" - }, - "method": "get", - "options": [], - "request_body": "", - "url": "https://api-adresse.data.gouv.fr/search/?q=10%20Rue%20Jangot" - }, - "response": { - "binary": false, - "body": "{\"type\": \"FeatureCollection\", \"version\": \"draft\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [4.842569, 45.751718]}, \"properties\": {\"label\": \"10 Rue Jangot 69007 Lyon\", \"score\": 0.8677708846033279, \"housenumber\": \"10\", \"id\": \"69387_3650_00010\", \"type\": \"housenumber\", \"x\": 843232.29, \"y\": 6518573.31, \"importance\": 0.5454797306366062, \"name\": \"10 Rue Jangot\", \"postcode\": \"69007\", \"citycode\": \"69387\", \"city\": \"Lyon\", \"district\": \"Lyon 7e Arrondissement\", \"context\": \"69, Rh\u00f4ne, Auvergne-Rh\u00f4ne-Alpes\", \"street\": \"Rue Jangot\"}}, {\"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [2.440319, 50.371266]}, \"properties\": {\"label\": \"Rue Jangon 62127 Bailleul-aux-Cornailles\", \"score\": 0.5269641371131077, \"id\": \"62070_0100\", \"type\": \"street\", \"x\": 660129.18, \"y\": 7030540.46, \"importance\": 0.25814396978264664, \"name\": \"Rue Jangon\", \"postcode\": \"62127\", \"citycode\": \"62070\", \"city\": \"Bailleul-aux-Cornailles\", \"context\": \"62, Pas-de-Calais, Hauts-de-France\"}}], \"attribution\": \"BAN\", \"licence\": \"ETALAB-2.0\", \"query\": \"10 Rue Jangot\", \"limit\": 5}", - "headers": { - "Server":"nginx/1.10.3", - "Date":"Thu, 25 Jun 2020 11:23:54 GMT", - "Content-type":"application/json; charset=utf-8", - "Vary":"Accept-Encoding", - "X-cache-status":"MISS", - "Access-control-allow-origin":"*", - "Access-control-allow-headers":"X-Requested-With,Content-Type", - "Content-encoding":"gzip" - }, - "status_code": 200, - "type": "ok" - } - } -] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json b/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json deleted file mode 100644 index 26bbccc47..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json +++ /dev/null @@ -1,63 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": [], - "method": "get", - "options": [], - "request_body": "", - "url": "https://maps.googleapis.com/maps/api/place/details/json?key=toto&placeid=ChIJrW0QikTq9EcR96jk2OnO75w" - }, - "response": { - "binary": false, - "body": "{\n \"html_attributions\" : [],\n \"result\" : {\n \"address_components\" : [\n {\n \"long_name\" : \"10bis\",\n \"short_name\" : \"10bis\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"adr_address\" : \"\\u003cspan class=\\\"street-address\\\"\\u003e10bis Rue Jangot\\u003c/span\\u003e, \\u003cspan class=\\\"postal-code\\\"\\u003e69007\\u003c/span\\u003e \\u003cspan class=\\\"locality\\\"\\u003eLyon\\u003c/span\\u003e, \\u003cspan class=\\\"country-name\\\"\\u003eFrance\\u003c/span\\u003e\",\n \"formatted_address\" : \"10bis Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.751725,\n \"lng\" : 4.8424966\n },\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.7531097802915,\n \"lng\" : 4.843951380291502\n },\n \"southwest\" : {\n \"lat\" : 45.7504118197085,\n \"lng\" : 4.841253419708497\n }\n }\n },\n \"icon\" : \"https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png\",\n \"id\" : \"4a3482a7a74c6203048adf713b736186c4ace7cd\",\n \"name\" : \"10bis Rue Jangot\",\n \"place_id\" : \"ChIJrW0QikTq9EcR96jk2OnO75w\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"reference\" : \"ChIJrW0QikTq9EcR96jk2OnO75w\",\n \"scope\" : \"GOOGLE\",\n \"types\" : [ \"street_address\" ],\n \"url\" : \"https://maps.google.com/?q=10bis+Rue+Jangot,+69007+Lyon,+France&ftid=0x47f4ea448a106dad:0x9cefcee9d8e4a8f7\",\n \"utc_offset\" : 120,\n \"vicinity\" : \"Lyon\"\n },\n \"status\" : \"OK\"\n}\n", - "headers": { - "Content-Type": "application/json; charset=UTF-8", - "Date": "Thu, 22 Aug 2019 13:18:50 GMT", - "Expires": "Thu, 22 Aug 2019 13:23:50 GMT", - "Cache-Control": "public, max-age=300", - "Server": "scaffolding on HTTPServer2", - "X-XSS-Protection": "0", - "X-Frame-Options": "SAMEORIGIN", - "Server-Timing": "gfet4t7; dur=87", - "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,43,39\"", - "Accept-Ranges": "none", - "Vary": "Accept-Language,Accept-Encoding", - "Transfer-Encoding": "chunked" - }, - "status_code": 200, - "type": "ok" - } - }, - { - "request": { - "body": "", - "headers": [], - "method": "get", - "options": [], - "request_body": "", - "url": "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=toto&language=en&latlng=45.751718,4.842569&result_type=street_address" - }, - "response": { - "binary": false, - "body": "{\n \"plus_code\" : {\n \"compound_code\" : \"QR2V+M2 Lyon, France\",\n \"global_code\" : \"8FQ6QR2V+M2\"\n },\n \"results\" : [\n {\n \"address_components\" : [\n {\n \"long_name\" : \"10bis\",\n \"short_name\" : \"10bis\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"formatted_address\" : \"10bis Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.751725,\n \"lng\" : 4.8424966\n },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.7530739802915,\n \"lng\" : 4.843845580291503\n },\n \"southwest\" : {\n \"lat\" : 45.7503760197085,\n \"lng\" : 4.841147619708499\n }\n }\n },\n \"place_id\" : \"ChIJrW0QikTq9EcR96jk2OnO75w\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"types\" : [ \"street_address\" ]\n },\n {\n \"address_components\" : [\n {\n \"long_name\" : \"9\",\n \"short_name\" : \"9\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"formatted_address\" : \"9 Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.7518165,\n \"lng\" : 4.8427168\n },\n \"location_type\" : \"RANGE_INTERPOLATED\",\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.7531654802915,\n \"lng\" : 4.844065780291502\n },\n \"southwest\" : {\n \"lat\" : 45.7504675197085,\n \"lng\" : 4.841367819708497\n }\n }\n },\n \"place_id\" : \"EiA5IFJ1ZSBKYW5nb3QsIDY5MDA3IEx5b24sIEZyYW5jZSIaEhgKFAoSCR8N2ItE6vRHEW9tyPnhQsUIEAk\",\n \"types\" : [ \"street_address\" ]\n }\n ],\n \"status\" : \"OK\"\n}\n", - "headers": { - "Content-Type": "application/json; charset=UTF-8", - "Date": "Thu, 22 Aug 2019 13:18:50 GMT", - "Expires": "Thu, 22 Aug 2019 13:19:20 GMT", - "Cache-Control": "public, max-age=30", - "Access-Control-Allow-Origin": "*", - "Server": "mafe", - "X-XSS-Protection": "0", - "X-Frame-Options": "SAMEORIGIN", - "Server-Timing": "gfet4t7; dur=30", - "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,43,39\"", - "Accept-Ranges": "none", - "Vary": "Accept-Encoding", - "Transfer-Encoding": "chunked" - }, - "status_code": 200, - "type": "ok" - } - } -] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json b/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json deleted file mode 100644 index f06de36bc..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json +++ /dev/null @@ -1,63 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": [], - "method": "get", - "options": [], - "request_body": "", - "url": "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=toto&language=en&address=10%20rue%20Jangot" - }, - "response": { - "binary": false, - "body": "{\n \"results\" : [\n {\n \"address_components\" : [\n {\n \"long_name\" : \"10\",\n \"short_name\" : \"10\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"formatted_address\" : \"10 Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.75164940000001,\n \"lng\" : 4.8424032\n },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.75299838029151,\n \"lng\" : 4.843752180291502\n },\n \"southwest\" : {\n \"lat\" : 45.75030041970851,\n \"lng\" : 4.841054219708497\n }\n }\n },\n \"place_id\" : \"ChIJtW0QikTq9EcRLI4Vy6bRx0U\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"types\" : [ \"street_address\" ]\n }\n ],\n \"status\" : \"OK\"\n}\n", - "headers": { - "Content-Type": "application/json; charset=UTF-8", - "Date": "Thu, 22 Aug 2019 13:05:39 GMT", - "Expires": "Thu, 22 Aug 2019 13:06:09 GMT", - "Cache-Control": "public, max-age=30", - "Access-Control-Allow-Origin": "*", - "Server": "mafe", - "X-XSS-Protection": "0", - "X-Frame-Options": "SAMEORIGIN", - "Server-Timing": "gfet4t7; dur=44", - "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,43,39\"", - "Accept-Ranges": "none", - "Vary": "Accept-Encoding", - "Transfer-Encoding": "chunked" - }, - "status_code": 200, - "type": "ok" - } - }, - { - "request": { - "body": "", - "headers": [], - "method": "get", - "options": [], - "request_body": "", - "url": "https://maps.googleapis.com/maps/api/place/details/json?key=toto&placeid=ChIJtW0QikTq9EcRLI4Vy6bRx0U" - }, - "response": { - "binary": false, - "body": "{\n \"html_attributions\" : [],\n \"result\" : {\n \"address_components\" : [\n {\n \"long_name\" : \"10\",\n \"short_name\" : \"10\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"adr_address\" : \"\\u003cspan class=\\\"street-address\\\"\\u003e10 Rue Jangot\\u003c/span\\u003e, \\u003cspan class=\\\"postal-code\\\"\\u003e69007\\u003c/span\\u003e \\u003cspan class=\\\"locality\\\"\\u003eLyon\\u003c/span\\u003e, \\u003cspan class=\\\"country-name\\\"\\u003eFrance\\u003c/span\\u003e\",\n \"formatted_address\" : \"10 Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.75164940000001,\n \"lng\" : 4.842403200000001\n },\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.7530412802915,\n \"lng\" : 4.843668630291503\n },\n \"southwest\" : {\n \"lat\" : 45.75034331970851,\n \"lng\" : 4.840970669708498\n }\n }\n },\n \"icon\" : \"https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png\",\n \"id\" : \"61b9418d092d2ed05ddd65a55dddefda5b9628cc\",\n \"name\" : \"10 Rue Jangot\",\n \"place_id\" : \"ChIJtW0QikTq9EcRLI4Vy6bRx0U\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"reference\" : \"ChIJtW0QikTq9EcRLI4Vy6bRx0U\",\n \"scope\" : \"GOOGLE\",\n \"types\" : [ \"street_address\" ],\n \"url\" : \"https://maps.google.com/?q=10+Rue+Jangot,+69007+Lyon,+France&ftid=0x47f4ea448a106db5:0x45c7d1a6cb158e2c\",\n \"utc_offset\" : 120,\n \"vicinity\" : \"Lyon\"\n },\n \"status\" : \"OK\"\n}\n", - "headers": { - "Content-Type": "application/json; charset=UTF-8", - "Date": "Thu, 22 Aug 2019 13:05:39 GMT", - "Expires": "Thu, 22 Aug 2019 13:10:39 GMT", - "Cache-Control": "public, max-age=300", - "Server": "scaffolding on HTTPServer2", - "X-XSS-Protection": "0", - "X-Frame-Options": "SAMEORIGIN", - "Server-Timing": "gfet4t7; dur=86", - "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,43,39\"", - "Accept-Ranges": "none", - "Vary": "Accept-Language,Accept-Encoding", - "Transfer-Encoding": "chunked" - }, - "status_code": 200, - "type": "ok" - } - } -] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json b/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json deleted file mode 100644 index a42c62f08..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json +++ /dev/null @@ -1,36 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": [], - "method": "get", - "options": [], - "request_body": "", - "url": "https://open.mapquestapi.com/geocoding/v1/reverse?key=secret_key&location=45.751718,4.842569&maxResults=10" - }, - "response": { - "binary": false, - "body": "{\"info\":{\"statuscode\":0,\"copyright\":{\"text\":\"\\u00A9 2019 MapQuest, Inc.\",\"imageUrl\":\"http://api.mqcdn.com/res/mqlogo.gif\",\"imageAltText\":\"\\u00A9 2019 MapQuest, Inc.\"},\"messages\":[]},\"options\":{\"maxResults\":1,\"thumbMaps\":true,\"ignoreLatLngInput\":false},\"results\":[{\"providedLocation\":{\"latLng\":{\"lat\":45.751718,\"lng\":4.842569}},\"locations\":[{\"street\":\"10 Rue Jangot\",\"adminArea6\":\"\",\"adminArea6Type\":\"Neighborhood\",\"adminArea5\":\"Lyon\",\"adminArea5Type\":\"City\",\"adminArea4\":\"\",\"adminArea4Type\":\"County\",\"adminArea3\":\"Auvergne-Rh\\u00F4ne-Alpes\",\"adminArea3Type\":\"State\",\"adminArea1\":\"FR\",\"adminArea1Type\":\"Country\",\"postalCode\":\"69007\",\"geocodeQualityCode\":\"P1AAA\",\"geocodeQuality\":\"POINT\",\"dragPoint\":false,\"sideOfStreet\":\"N\",\"linkId\":\"0\",\"unknownInput\":\"\",\"type\":\"s\",\"latLng\":{\"lat\":45.751714,\"lng\":4.842566},\"displayLatLng\":{\"lat\":45.751714,\"lng\":4.842566},\"mapUrl\":\"http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=-570915433\"}]}]}", - "headers": { - "Access-Control-Allow-Methods": "OPTIONS,GET,POST", - "Access-Control-Allow-Origin": "*", - "Cache-Control": "no-cache, must-revalidate", - "Content-Type": "application/json;charset=UTF-8", - "Date": "Thu, 14 Mar 2019 09:27:01 GMT", - "Expires": "Mon, 20 Dec 1998 01:00:00 GMT", - "GeocodeTransactionCount": "0", - "Last-Modified": "Thu, 14 Mar 2019 09:27:01 GMT", - "Pragma": "no-cache", - "ReverseGeocodeTransactionCount": "1", - "Server": "Apache-Coyote/1.1", - "Set-Cookie": "JSESSIONID=something; Path=/; HttpOnly", - "status": "success", - "transactionWeight": "1.0", - "Content-Length": "1063", - "Connection": "keep-alive" - }, - "status_code": 200, - "type": "ok" - } - } -] diff --git a/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json b/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json deleted file mode 100644 index 4b23ed257..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json +++ /dev/null @@ -1,36 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": [], - "method": "get", - "options": [], - "request_body": "", - "url": "https://open.mapquestapi.com/geocoding/v1/address?key=secret_key&location=10%20rue%20Jangot&maxResults=10" - }, - "response": { - "binary": false, - "body": "{\"info\":{\"statuscode\":0,\"copyright\":{\"text\":\"\\u00A9 2019 MapQuest, Inc.\",\"imageUrl\":\"http://api.mqcdn.com/res/mqlogo.gif\",\"imageAltText\":\"\\u00A9 2019 MapQuest, Inc.\"},\"messages\":[]},\"options\":{\"maxResults\":10,\"thumbMaps\":true,\"ignoreLatLngInput\":false},\"results\":[{\"providedLocation\":{\"location\":\"10 rue Jangot\"},\"locations\":[{\"street\":\"10 Rue Jangot\",\"adminArea6\":\"7e\",\"adminArea6Type\":\"Neighborhood\",\"adminArea5\":\"Lyon\",\"adminArea5Type\":\"City\",\"adminArea4\":\"Lyon\",\"adminArea4Type\":\"County\",\"adminArea3\":\"Auvergne-Rh\\u00F4ne-Alpes\",\"adminArea3Type\":\"State\",\"adminArea1\":\"FR\",\"adminArea1Type\":\"Country\",\"postalCode\":\"69007\",\"geocodeQualityCode\":\"P1AXX\",\"geocodeQuality\":\"POINT\",\"dragPoint\":false,\"sideOfStreet\":\"N\",\"linkId\":\"0\",\"unknownInput\":\"\",\"type\":\"s\",\"latLng\":{\"lat\":45.751714,\"lng\":4.842566},\"displayLatLng\":{\"lat\":45.751714,\"lng\":4.842566},\"mapUrl\":\"http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=1358091752\"}]}]}", - "headers": { - "Access-Control-Allow-Methods": "OPTIONS,GET,POST", - "Access-Control-Allow-Origin": "*", - "Cache-Control": "no-cache, must-revalidate", - "Content-Type": "application/json;charset=UTF-8", - "Date": "Thu, 14 Mar 2019 09:27:01 GMT", - "Expires": "Mon, 20 Dec 1998 01:00:00 GMT", - "GeocodeTransactionCount": "1", - "Last-Modified": "Thu, 14 Mar 2019 09:27:01 GMT", - "Pragma": "no-cache", - "ReverseGeocodeTransactionCount": "0", - "Server": "Apache-Coyote/1.1", - "Set-Cookie": "JSESSIONID=something; Path=/; HttpOnly", - "status": "success", - "transactionWeight": "1.0", - "Content-Length": "1055", - "Connection": "keep-alive" - }, - "status_code": 200, - "type": "ok" - } - } -] diff --git a/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json b/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json deleted file mode 100644 index ad9beef81..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": { - "User-Agent": "Test instance mobilizon.test - Mobilizon 1.0.0-beta.1" - }, - "method": "get", - "options": [], - "request_body": "", - "url": "https://nominatim.openstreetmap.org/reverse?format=geocodejson&lat=45.751718&lon=4.842569&accept-language=en&addressdetails=1&namedetails=1" - }, - "response": { - "binary": false, - "body": "{\"type\":\"FeatureCollection\",\"geocoding\":{\"version\":\"0.1.0\",\"attribution\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"licence\":\"ODbL\",\"query\":\"45.751718,4.842569\"},\"features\":[{\"type\":\"Feature\",\"properties\":{\"geocoding\":{\"place_id\":41453794,\"osm_type\":\"node\",\"osm_id\":3078260611,\"type\":\"house\",\"accuracy\":0,\"label\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"name\":null,\"housenumber\":\"10\",\"street\":\"Rue Jangot\",\"postcode\":\"69007\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"admin\":{\"level2\":\"France\",\"level3\":\"Metropolitan France\",\"level4\":\"Auvergne-Rhône-Alpes\",\"level5\":\"Departemental constituency of Rhône\",\"level6\":\"Métropole de Lyon\",\"level7\":\"Lyon\",\"level8\":\"Lyon\",\"level9\":\"Lyon 7e Arrondissement\"}}},\"geometry\":{\"type\":\"Point\",\"coordinates\":[4.8425657,45.7517141]}}]}", - "headers": { - "Date": "Tue, 12 Nov 2019 12:21:45 GMT", - "Server": "Apache/2.4.29 (Ubuntu)", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "OPTIONS,GET", - "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", - "Expect-CT": "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\"", - "Upgrade": "h2", - "Connection": "Upgrade, close", - "Transfer-Encoding": "chunked", - "Content-Type": "application/json; charset=UTF-8" - }, - "status_code": 200, - "type": "ok" - } - } -] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json b/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json deleted file mode 100644 index 395cf11ee..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": { - "User-Agent": "Test instance mobilizon.test - Mobilizon 1.0.0-beta.1" - }, - "method": "get", - "options": [], - "request_body": "", - "url": "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1&namedetails=1" - }, - "response": { - "binary": false, - "body": "{\"type\":\"FeatureCollection\",\"geocoding\":{\"version\":\"0.1.0\",\"attribution\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"licence\":\"ODbL\",\"query\":\"10 rue Jangot\"},\"features\":[{\"type\":\"Feature\",\"properties\":{\"geocoding\":{\"place_id\":41453794,\"osm_type\":\"node\",\"osm_id\":3078260611,\"type\":\"house\",\"label\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"name\":null,\"housenumber\":\"10\",\"street\":\"Rue Jangot\",\"postcode\":\"69007\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"admin\":{\"level2\":\"France\",\"level3\":\"Metropolitan France\",\"level4\":\"Auvergne-Rhône-Alpes\",\"level5\":\"Departemental constituency of Rhône\",\"level6\":\"Métropole de Lyon\",\"level7\":\"Lyon\",\"level8\":\"Lyon\",\"level9\":\"Lyon 7e Arrondissement\"}}},\"geometry\":{\"type\":\"Point\",\"coordinates\":[4.8425657,45.7517141]}}]}", - "headers": { - "Date": "Tue, 12 Nov 2019 12:21:46 GMT", - "Server": "Apache/2.4.29 (Ubuntu)", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "OPTIONS,GET", - "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", - "Expect-CT": "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\"", - "Upgrade": "h2", - "Connection": "Upgrade, close", - "Transfer-Encoding": "chunked", - "Content-Type": "application/json; charset=UTF-8" - }, - "status_code": 200, - "type": "ok" - } - } -] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json b/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json deleted file mode 100644 index 8ff49672e..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": [], - "method": "get", - "options": [], - "request_body": "", - "url": "https://photon.komoot.de/reverse?lon=4.842569&lat=45.751718" - }, - "response": { - "binary": false, - "body": "{\"features\":[{\"geometry\":{\"coordinates\":[4.8416864,45.7605435],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":4662865602,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"leisure\",\"city\":\"Lyon\",\"street\":\"Rue Pravaz\",\"osm_value\":\"fitness_centre\",\"postcode\":\"69003\",\"name\":\"L'appart Fitness\",\"state\":\"Auvergne-Rhône-Alpes\"}}],\"type\":\"FeatureCollection\"}", - "headers": { - "Server": "nginx/1.9.3 (Ubuntu)", - "Date": "Thu, 14 Mar 2019 10:46:45 GMT", - "Content-Type": "application/json;charset=utf-8", - "Transfer-Encoding": "chunked", - "Connection": "keep-alive", - "Access-Control-Allow-Origin": "*" - }, - "status_code": 200, - "type": "ok" - } - } -] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/geospatial/photon/search.json b/test/fixtures/vcr_cassettes/geospatial/photon/search.json deleted file mode 100644 index 45a644e63..000000000 --- a/test/fixtures/vcr_cassettes/geospatial/photon/search.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "request": { - "body": "", - "headers": [], - "method": "get", - "options": [], - "request_body": "", - "url": "https://photon.komoot.de/api/?q=10%20rue%20Jangot&lang=en&limit=10" - }, - "response": { - "binary": false, - "body": "{\"features\":[{\"geometry\":{\"coordinates\":[4.8425657,45.7517141],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":3078260611,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"place\",\"housenumber\":\"10\",\"city\":\"Lyon\",\"street\":\"Rue Jangot\",\"osm_value\":\"house\",\"postcode\":\"69007\",\"state\":\"Auvergne-Rhône-Alpes\"}},{\"geometry\":{\"coordinates\":[4.8424254,45.7517056],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":3078260612,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"place\",\"housenumber\":\"10bis\",\"city\":\"Lyon\",\"street\":\"Rue Jangot\",\"osm_value\":\"house\",\"postcode\":\"69007\",\"state\":\"Auvergne-Rhône-Alpes\"}}],\"type\":\"FeatureCollection\"}", - "headers": { - "Server": "nginx/1.9.3 (Ubuntu)", - "Date": "Thu, 14 Mar 2019 10:46:43 GMT", - "Content-Type": "application/json;charset=utf-8", - "Transfer-Encoding": "chunked", - "Connection": "keep-alive", - "Access-Control-Allow-Origin": "*" - }, - "status_code": 200, - "type": "ok" - } - } -] \ No newline at end of file diff --git a/test/graphql/api/report_test.exs b/test/graphql/api/report_test.exs index 5b0c85fb8..fbf4e377b 100644 --- a/test/graphql/api/report_test.exs +++ b/test/graphql/api/report_test.exs @@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do import Mobilizon.Factory alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Service.Formatter.HTML diff --git a/test/graphql/resolvers/post_test.exs b/test/graphql/resolvers/post_test.exs new file mode 100644 index 000000000..ddc365eb1 --- /dev/null +++ b/test/graphql/resolvers/post_test.exs @@ -0,0 +1,627 @@ +defmodule Mobilizon.GraphQL.Resolvers.PostTest do + use Mobilizon.Web.ConnCase + + import Mobilizon.Factory + + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Posts.Post + alias Mobilizon.Users.User + + alias Mobilizon.GraphQL.AbsintheHelpers + + @post_fragment """ + fragment PostFragment on Post { + id + title + slug + url + body + author { + id + preferredUsername + name + avatar { + url + } + } + attributedTo { + id + preferredUsername + name + avatar { + url + } + } + visibility + insertedAt + updatedAt + draft + } + """ + + @get_group_posts """ + query($name: String!, $page: Int, $limit: Int) { + group(preferredUsername: $name) { + id + posts(page: $page, limit: $limit) { + elements { + id, + title, + }, + total + }, + } + } + """ + + @post_query """ + query Post($slug: String!) { + post(slug: $slug) { + ...PostFragment + } + } + #{@post_fragment} + """ + + @create_post """ + mutation CreatePost($title: String!, $body: String, $attributedToId: ID!, $draft: Boolean) { + createPost(title: $title, body: $body, attributedToId: $attributedToId, draft: $draft) { + ...PostFragment + } + } + #{@post_fragment} + """ + + @update_post """ + mutation UpdatePost($id: ID!, $title: String, $body: String, $attributedToId: ID, $draft: Boolean) { + updatePost(id: $id, title: $title, body: $body, attributedToId: $attributedToId, draft: $draft) { + ...PostFragment + } + } + #{@post_fragment} + """ + + @delete_post """ + mutation DeletePost($id: ID!) { + deletePost(id: $id) { + id + } + } + """ + + @post_title "my post" + @updated_post_title "my updated post" + + setup do + %User{} = user = insert(:user) + %Actor{} = actor = insert(:actor, user: user) + %Actor{} = group = insert(:group) + %Post{} = post = insert(:post, attributed_to: group, author: actor) + + %Post{} = + post_unlisted = insert(:post, attributed_to: group, author: actor, visibility: :unlisted) + + %Post{} = post_draft = insert(:post, attributed_to: group, author: actor, draft: true) + %Member{} = insert(:member, parent: group, actor: actor, role: :member) + + %Post{} = + post_private = insert(:post, attributed_to: group, author: actor, visibility: :private) + + {:ok, + user: user, + group: group, + post: post, + post_unlisted: post_unlisted, + post_draft: post_draft, + post_private: post_private} + end + + describe "Resolver: Get group's posts" do + test "find_posts_for_group/3", %{ + conn: conn, + user: user, + group: group, + post: post, + post_unlisted: post_unlisted, + post_draft: post_draft, + post_private: post_private + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @get_group_posts, + variables: %{ + name: group.preferred_username + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["group"]["posts"]["total"] == 4 + + assert res["data"]["group"]["posts"]["elements"] |> Enum.map(& &1["id"]) |> MapSet.new() == + MapSet.new([ + post.id, + post_unlisted.id, + post_draft.id, + post_private.id + ]) + end + + test "find_posts_for_group/3 when not member of group", %{ + conn: conn, + group: group, + post: post + } do + %User{} = user = insert(:user) + %Actor{} = insert(:actor, user: user) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @get_group_posts, + variables: %{ + name: group.preferred_username + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["group"]["posts"]["total"] == 1 + + assert res["data"]["group"]["posts"]["elements"] |> Enum.map(& &1["id"]) == [post.id] + end + + test "find_posts_for_group/3 when not connected", %{ + conn: conn, + group: group, + post: post + } do + res = + conn + |> AbsintheHelpers.graphql_query( + query: @get_group_posts, + variables: %{ + name: group.preferred_username + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["group"]["posts"]["total"] == 1 + + assert res["data"]["group"]["posts"]["elements"] |> Enum.map(& &1["id"]) == [post.id] + end + end + + describe "Resolver: Get a specific post" do + test "get_post/3 for a public post", %{ + conn: conn, + user: user, + post: post + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post.slug + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["post"]["title"] == post.title + end + + test "get_post/3 for a non-existing post", %{ + conn: conn, + user: user + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: "not existing" + } + ) + + assert hd(res["errors"])["message"] == "No such post" + end + + test "get_post/3 for an unlisted post", %{ + conn: conn, + user: user, + post_unlisted: post_unlisted + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_unlisted.slug + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["post"]["title"] == post_unlisted.title + + assert res["data"]["post"]["visibility"] == + post_unlisted.visibility |> to_string() |> String.upcase() + end + + test "get_post/3 for a private post", %{ + conn: conn, + user: user, + post_private: post_private + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_private.slug + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["post"]["title"] == post_private.title + + assert res["data"]["post"]["visibility"] == + post_private.visibility |> to_string() |> String.upcase() + end + + test "get_post/3 for a draft post", %{ + conn: conn, + user: user, + post_draft: post_draft + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_draft.slug + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["post"]["title"] == post_draft.title + assert res["data"]["post"]["draft"] == true + end + + test "get_post/3 without being a member for a public post", %{ + conn: conn, + post: post + } do + %User{} = user = insert(:user) + %Actor{} = insert(:actor, user: user) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post.slug + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["post"]["title"] == post.title + end + + test "get_post/3 without being connected for a public post", %{ + conn: conn, + post: post + } do + res = + conn + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post.slug + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["post"]["title"] == post.title + end + + test "get_post/3 without being a member for an unlisted post", %{ + conn: conn, + post_unlisted: post_unlisted + } do + %User{} = user = insert(:user) + %Actor{} = insert(:actor, user: user) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_unlisted.slug + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["post"]["title"] == post_unlisted.title + + assert res["data"]["post"]["visibility"] == + post_unlisted.visibility |> to_string() |> String.upcase() + end + + test "get_post/3 without being a member for a private post", %{ + conn: conn, + post_private: post_private + } do + %User{} = user = insert(:user) + %Actor{} = insert(:actor, user: user) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_private.slug + } + ) + + assert hd(res["errors"])["message"] == "No such post" + end + + test "get_post/3 without being connected for an unlisted post still gives the post", %{ + conn: conn, + post_unlisted: post_unlisted + } do + res = + conn + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_unlisted.slug + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["post"]["title"] == post_unlisted.title + + assert res["data"]["post"]["visibility"] == + post_unlisted.visibility |> to_string() |> String.upcase() + end + + test "get_post/3 without being connected for a private post", %{ + conn: conn, + post_private: post_private + } do + res = + conn + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_private.slug + } + ) + + assert hd(res["errors"])["message"] == "No such post" + end + + test "get_post/3 without being a member for a draft post", %{ + conn: conn, + post_draft: post_draft + } do + %User{} = user = insert(:user) + %Actor{} = insert(:actor, user: user) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_draft.slug + } + ) + + assert hd(res["errors"])["message"] == "No such post" + end + + test "get_post/3 without being connected for a draft post", %{ + conn: conn, + post_draft: post_draft + } do + res = + conn + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_draft.slug + } + ) + + assert hd(res["errors"])["message"] == "No such post" + end + end + + describe "Resolver: Create a post" do + test "create_post/3 creates a post for a group", %{ + conn: conn, + user: user, + group: group + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @create_post, + variables: %{ + title: @post_title, + body: "My new post is here", + attributedToId: group.id + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["createPost"]["title"] == @post_title + id = res["data"]["createPost"]["id"] + assert res["data"]["createPost"]["slug"] == "my-post-#{ShortUUID.encode!(id)}" + end + + test "create_post/3 doesn't create a post if no group is defined", %{ + conn: conn, + user: user + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @create_post, + variables: %{ + title: @post_title, + body: "some body", + attributedToId: nil + } + ) + + assert Enum.map(res["errors"], & &1["message"]) == [ + "Argument \"attributedToId\" has invalid value $attributedToId.", + "Variable \"attributedToId\": Expected non-null, found null." + ] + end + + test "create_post/3 doesn't create a post if the actor is not a member of the group", + %{ + conn: conn, + group: group + } do + %User{} = user = insert(:user) + %Actor{} = insert(:actor, user: user) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @create_post, + variables: %{ + title: @post_title, + body: "My body", + attributedToId: group.id + } + ) + + assert Enum.map(res["errors"], & &1["message"]) == [ + "Actor id is not member of group" + ] + end + end + + describe "Resolver: Update a post" do + test "update_post/3 updates a post for a group", %{ + conn: conn, + user: user, + group: group + } do + %Post{id: post_id} = insert(:post, attributed_to: group) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_post, + variables: %{ + id: post_id, + title: @updated_post_title + } + ) + + assert is_nil(res["errors"]) + + assert res["data"]["updatePost"]["title"] == @updated_post_title + end + end + + describe "Resolver: Delete a post" do + test "delete_post/3 deletes a post", %{ + conn: conn, + user: user, + group: group + } do + %Post{id: post_id, slug: post_slug} = + insert(:post, + attributed_to: group + ) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @delete_post, + variables: %{ + id: post_id + } + ) + + assert is_nil(res["errors"]) + assert res["data"]["deletePost"]["id"] == post_id + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @post_query, + variables: %{ + slug: post_slug + } + ) + + assert hd(res["errors"])["message"] == "No such post" + end + + test "delete_post/3 deletes a post not found", %{ + conn: conn, + user: user + } do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @delete_post, + variables: %{ + id: "not found" + } + ) + + assert hd(res["errors"])["message"] == "Post ID is not a valid ID" + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @delete_post, + variables: %{ + id: "d276ef98-8433-48d7-890e-c24eda0dcdbe" + } + ) + + assert hd(res["errors"])["message"] == "Post doesn't exist" + end + end +end diff --git a/test/graphql/resolvers/user_test.exs b/test/graphql/resolvers/user_test.exs index 477e9fed3..70637a328 100644 --- a/test/graphql/resolvers/user_test.exs +++ b/test/graphql/resolvers/user_test.exs @@ -5,9 +5,9 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do import Mobilizon.Factory - alias Mobilizon.{Actors, Config, Conversations, Events, Users} + alias Mobilizon.{Actors, Config, Discussions, Events, Users} alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Service.Auth.Authenticator alias Mobilizon.Users.User @@ -1388,7 +1388,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end assert_raise Ecto.NoResultsError, fn -> - Conversations.get_comment!(comment_id) + Discussions.get_comment!(comment_id) end # Actors are not deleted but emptied (to keep the username reserved) diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs index 8b863df20..fd6ed2f7b 100644 --- a/test/mobilizon/actors/actors_test.exs +++ b/test/mobilizon/actors/actors_test.exs @@ -5,9 +5,9 @@ defmodule Mobilizon.ActorsTest do import Mobilizon.Factory - alias Mobilizon.{Actors, Config, Conversations, Events, Tombstone, Users} + alias Mobilizon.{Actors, Config, Discussions, Events, Tombstone, Users} alias Mobilizon.Actors.{Actor, Bot, Follower, Member} - alias Mobilizon.Conversations.Comment + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Media.File, as: FileModel alias Mobilizon.Service.Workers @@ -291,6 +291,56 @@ defmodule Mobilizon.ActorsTest do assert actor = actor_fetched end + test "perform delete the actor actually deletes the actor", %{ + actor: %Actor{avatar: %{url: avatar_url}, banner: %{url: banner_url}, id: actor_id} = actor + } do + %Event{url: event1_url} = event1 = insert(:event, organizer_actor: actor) + insert(:event, organizer_actor: actor) + + %Comment{url: comment1_url} = comment1 = insert(:comment, actor: actor) + insert(:comment, actor: actor) + + %URI{path: "/media/" <> avatar_path} = URI.parse(avatar_url) + %URI{path: "/media/" <> banner_path} = URI.parse(banner_url) + + assert File.exists?( + Config.get!([Uploader.Local, :uploads]) <> + "/" <> avatar_path + ) + + assert File.exists?( + Config.get!([Uploader.Local, :uploads]) <> + "/" <> banner_path + ) + + assert {:ok, %Actor{}} = Actors.perform(:delete_actor, actor) + + assert %Actor{ + name: nil, + summary: nil, + suspended: true, + avatar: nil, + banner: nil, + user_id: nil + } = Actors.get_actor(actor_id) + + assert {:error, :event_not_found} = Events.get_event(event1.id) + assert %Tombstone{} = Tombstone.find_tombstone(event1_url) + assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id) + refute is_nil(deleted_at) + assert %Tombstone{} = Tombstone.find_tombstone(comment1_url) + + refute File.exists?( + Config.get!([Uploader.Local, :uploads]) <> + "/" <> avatar_path + ) + + refute File.exists?( + Config.get!([Uploader.Local, :uploads]) <> + "/" <> banner_path + ) + end + test "delete_actor/1 deletes the actor", %{ actor: %Actor{avatar: %{url: avatar_url}, banner: %{url: banner_url}, id: actor_id} = actor } do @@ -333,7 +383,7 @@ defmodule Mobilizon.ActorsTest do assert {:error, :event_not_found} = Events.get_event(event1.id) assert %Tombstone{} = Tombstone.find_tombstone(event1_url) - assert %Comment{deleted_at: deleted_at} = Conversations.get_comment(comment1.id) + assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id) refute is_nil(deleted_at) assert %Tombstone{} = Tombstone.find_tombstone(comment1_url) diff --git a/test/mobilizon/conversations_test.exs b/test/mobilizon/discussions_test.exs similarity index 65% rename from test/mobilizon/conversations_test.exs rename to test/mobilizon/discussions_test.exs index bd95205d1..5e9984e08 100644 --- a/test/mobilizon/conversations_test.exs +++ b/test/mobilizon/discussions_test.exs @@ -1,13 +1,11 @@ -defmodule Mobilizon.ConversationsTest do +defmodule Mobilizon.DiscussionsTest do use Mobilizon.DataCase import Mobilizon.Factory alias Mobilizon.Actors.Actor - alias Mobilizon.Conversations - alias Mobilizon.Conversations.Comment - alias Mobilizon.Service.Workers - alias Mobilizon.Storage.Page + alias Mobilizon.Discussions + alias Mobilizon.Discussions.Comment describe "comments" do @valid_attrs %{text: "some text"} @@ -16,13 +14,13 @@ defmodule Mobilizon.ConversationsTest do test "list_comments/0 returns all comments" do %Comment{id: comment_id} = insert(:comment) - comment_ids = Conversations.list_comments() |> Enum.map(& &1.id) + comment_ids = Discussions.list_comments() |> Enum.map(& &1.id) assert comment_ids == [comment_id] end test "get_comment!/1 returns the comment with given id" do %Comment{id: comment_id} = insert(:comment) - comment_fetched = Conversations.get_comment!(comment_id) + comment_fetched = Discussions.get_comment!(comment_id) assert comment_fetched.id == comment_id end @@ -30,7 +28,7 @@ defmodule Mobilizon.ConversationsTest do %Actor{} = actor = insert(:actor) comment_data = Map.merge(@valid_attrs, %{actor_id: actor.id}) - case Conversations.create_comment(comment_data) do + case Discussions.create_comment(comment_data) do {:ok, %Comment{} = comment} -> assert comment.text == "some text" assert comment.actor_id == actor.id @@ -41,13 +39,13 @@ defmodule Mobilizon.ConversationsTest do end test "create_comment/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Conversations.create_comment(@invalid_attrs) + assert {:error, %Ecto.Changeset{}} = Discussions.create_comment(@invalid_attrs) end test "update_comment/2 with valid data updates the comment" do %Comment{} = comment = insert(:comment) - case Conversations.update_comment(comment, @update_attrs) do + case Discussions.update_comment(comment, @update_attrs) do {:ok, %Comment{} = comment} -> assert comment.text == "some updated text" @@ -58,15 +56,15 @@ defmodule Mobilizon.ConversationsTest do test "update_comment/2 with invalid data returns error changeset" do %Comment{} = comment = insert(:comment) - assert {:error, %Ecto.Changeset{}} = Conversations.update_comment(comment, @invalid_attrs) - %Comment{} = comment_fetched = Conversations.get_comment!(comment.id) + assert {:error, %Ecto.Changeset{}} = Discussions.update_comment(comment, @invalid_attrs) + %Comment{} = comment_fetched = Discussions.get_comment!(comment.id) assert comment = comment_fetched end test "delete_comment/1 deletes the comment" do %Comment{} = comment = insert(:comment) - assert {:ok, %Comment{}} = Conversations.delete_comment(comment) - refute is_nil(Conversations.get_comment!(comment.id).deleted_at) + assert {:ok, %Comment{}} = Discussions.delete_comment(comment) + refute is_nil(Discussions.get_comment!(comment.id).deleted_at) end end end diff --git a/test/mobilizon/events/events_test.exs b/test/mobilizon/events/events_test.exs index 11bde5ed0..71f748049 100644 --- a/test/mobilizon/events/events_test.exs +++ b/test/mobilizon/events/events_test.exs @@ -141,7 +141,7 @@ defmodule Mobilizon.EventsTest do end test "list_public_events_for_actor/1", %{actor: actor, event: event} do - assert {:ok, [event_found], 1} = Events.list_public_events_for_actor(actor) + assert %Page{elements: [event_found], total: 1} = Events.list_public_events_for_actor(actor) assert event_found.title == event.title end @@ -149,7 +149,7 @@ defmodule Mobilizon.EventsTest do event1 = insert(:event, organizer_actor: actor) case Events.list_public_events_for_actor(actor, 1, 10) do - {:ok, events_found, 2} -> + %Page{elements: events_found, total: 2} -> event_ids = MapSet.new(events_found |> Enum.map(& &1.id)) assert event_ids == MapSet.new([event.id, event1.id]) @@ -162,7 +162,7 @@ defmodule Mobilizon.EventsTest do event1 = insert(:event, organizer_actor: actor) case Events.list_public_events_for_actor(actor, 1, 1) do - {:ok, [%Event{id: event_found_id}], 2} -> + %Page{elements: [%Event{id: event_found_id}], total: 2} -> assert event_found_id in [event.id, event1.id] err -> diff --git a/test/mobilizon/posts_test.exs b/test/mobilizon/posts_test.exs new file mode 100644 index 000000000..dd987674e --- /dev/null +++ b/test/mobilizon/posts_test.exs @@ -0,0 +1,82 @@ +defmodule Mobilizon.PostsTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + + alias Mobilizon.Actors.Actor + alias Mobilizon.Posts + alias Mobilizon.Posts.Post + + describe "posts" do + @valid_attrs %{body: "some text", title: "some title"} + @update_attrs %{body: "some updated text", title: "some updated title"} + @invalid_attrs %{body: nil} + + test "list_posts/0 returns all posts" do + group = insert(:group) + %Post{id: post_id} = insert(:post, attributed_to: group) + post_ids = Posts.get_posts_for_group(group).elements |> Enum.map(& &1.id) + assert post_ids == [post_id] + end + + test "get_post!/1 returns the post with given id" do + %Post{id: post_id} = insert(:post) + post_fetched = Posts.get_post(post_id) + assert post_fetched.id == post_id + end + + test "create_post/1 with valid data creates a post" do + %Actor{} = actor = insert(:actor) + %Actor{} = group = insert(:group) + post_data = Map.merge(@valid_attrs, %{author_id: actor.id, attributed_to_id: group.id}) + + case Posts.create_post(post_data) do + {:ok, %Post{} = post} -> + assert post.body == "some text" + assert post.author_id == actor.id + assert post.title == "some title" + assert post.slug == "some-title-" <> ShortUUID.encode!(post.id) + + err -> + flunk("Failed to create a post #{inspect(err)}") + end + end + + test "create_post/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Posts.create_post(@invalid_attrs) + end + + test "update_post/2 with valid data updates the post" do + %Actor{} = actor = insert(:actor) + %Actor{} = group = insert(:group) + post_data = Map.merge(@valid_attrs, %{author_id: actor.id, attributed_to_id: group.id}) + + {:ok, %Post{} = post} = Posts.create_post(post_data) + + case Posts.update_post(post, @update_attrs) do + {:ok, %Post{} = updated_post} -> + assert updated_post.body == @update_attrs.body + assert updated_post.title == @update_attrs.title + # Slug and URL don't change + assert updated_post.slug == post.slug + assert updated_post.url == post.url + + err -> + flunk("Failed to update a post #{inspect(err)}") + end + end + + test "update_post/2 with invalid data returns error changeset" do + %Post{} = post = insert(:post) + assert {:error, %Ecto.Changeset{}} = Posts.update_post(post, @invalid_attrs) + %Post{} = post_fetched = Posts.get_post(post.id) + assert post = post_fetched + end + + test "delete_post/1 deletes the post" do + %Post{} = post = insert(:post) + assert {:ok, %Post{}} = Posts.delete_post(post) + assert is_nil(Posts.get_post(post.id)) + end + end +end diff --git a/test/service/geospatial/addok_test.exs b/test/service/geospatial/addok_test.exs index 265cc3716..abcd450d8 100644 --- a/test/service/geospatial/addok_test.exs +++ b/test/service/geospatial/addok_test.exs @@ -1,91 +1,63 @@ defmodule Mobilizon.Service.Geospatial.AddokTest do - use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney - use Mobilizon.DataCase - import Mock + import Mox alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Addok - - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - - setup do - # Config.instance_user_agent/0 makes database calls so because of ownership connection - # we need to define it like this instead of a constant - # See https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html - {:ok, - httpoison_headers: [ - {"User-Agent", Config.instance_user_agent()} - ]} - end - - @endpoint get_in(Application.get_env(:mobilizon, Addok), [:endpoint]) - @fake_endpoint "https://domain.tld" + alias Mobilizon.Service.HTTP.BaseClient.Mock describe "search address" do - test "produces a valid search address", %{httpoison_headers: httpoison_headers} do - with_mock HTTPoison, get: fn _url, _headers, _options -> "{}" end do - Addok.search("10 Rue Jangot") - - assert_called( - HTTPoison.get( - "#{@endpoint}/search/?q=10%20Rue%20Jangot&limit=10", - httpoison_headers, - @http_options - ) - ) - end - end - - test "produces a valid search address with options", %{httpoison_headers: httpoison_headers} do - with_mock HTTPoison, get: fn _url, _headers, _options -> "{}" end do - Addok.search("10 Rue Jangot", - endpoint: @fake_endpoint, - limit: 5, - coords: %{lat: 49, lon: 12} - ) - - assert_called( - HTTPoison.get( - "#{@fake_endpoint}/search/?q=10%20Rue%20Jangot&limit=5&lat=49&lon=12", - httpoison_headers, - @http_options - ) - ) - end - end - test "returns a valid address from search" do - use_cassette "geospatial/addok/search" do - assert %Address{ - country: "France", - region: "69, Rhône, Auvergne-Rhône-Alpes", - locality: "Lyon", - description: "10 Rue Jangot", - postal_code: "69007", - street: "10 Rue Jangot", - geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326} - } == Addok.search("10 rue Jangot") |> hd - end + data = + File.read!("test/fixtures/geospatial/addok/search.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{ + method: :get, + url: "https://api-adresse.data.gouv.fr/search/?q=10%20rue%20Jangot&limit=10" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + assert %Address{ + country: "France", + region: "69, Rhône, Auvergne-Rhône-Alpes", + locality: "Lyon", + description: "10 Rue Jangot", + postal_code: "69007", + street: "10 Rue Jangot", + geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326} + } == Addok.search("10 rue Jangot") |> hd end test "returns a valid address from reverse geocode" do - use_cassette "geospatial/addok/geocode" do - assert %Address{ - country: "France", - region: "69, Rhône, Auvergne-Rhône-Alpes", - locality: "Lyon", - description: "10 Rue Jangot", - postal_code: "69007", - street: "10 Rue Jangot", - geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326} - } == Addok.geocode(4.842569, 45.751718) |> hd - end + data = + File.read!("test/fixtures/geospatial/addok/geocode.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{ + method: :get, + url: "https://api-adresse.data.gouv.fr/reverse/?lon=4.842569&lat=45.751718&limit=10" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + assert %Address{ + country: "France", + region: "69, Rhône, Auvergne-Rhône-Alpes", + locality: "Lyon", + description: "10 Rue Jangot", + postal_code: "69007", + street: "10 Rue Jangot", + geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326} + } == Addok.geocode(4.842569, 45.751718) |> hd end end end diff --git a/test/service/geospatial/google_maps_test.exs b/test/service/geospatial/google_maps_test.exs index 70a3e7c61..624fafbb6 100644 --- a/test/service/geospatial/google_maps_test.exs +++ b/test/service/geospatial/google_maps_test.exs @@ -1,17 +1,11 @@ defmodule Mobilizon.Service.Geospatial.GoogleMapsTest do - use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney - use Mobilizon.DataCase - import Mock + import Mox alias Mobilizon.Addresses.Address alias Mobilizon.Service.Geospatial.GoogleMaps - - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] + alias Mobilizon.Service.HTTP.BaseClient.Mock describe "search address" do test "without API Key triggers an error" do @@ -20,76 +14,117 @@ defmodule Mobilizon.Service.Geospatial.GoogleMapsTest do end end - test "produces a valid search address with options" do - with_mock HTTPoison, - get: fn _url, _headers, _options -> - {:ok, - %HTTPoison.Response{status_code: 200, body: "{\"status\": \"OK\", \"results\": []}"}} - end do - GoogleMaps.search("10 Rue Jangot", - limit: 5, - lang: "fr", - api_key: "toto" - ) - - assert_called( - HTTPoison.get( - "https://maps.googleapis.com/maps/api/geocode/json?limit=5&key=toto&language=fr&address=10%20Rue%20Jangot", - [], - @http_options - ) - ) - end - end - test "triggers an error with an invalid API Key" do + data = + File.read!("test/fixtures/geospatial/google_maps/api_key_invalid.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{ + method: :get, + url: + "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=secret_key&language=en&address=10%20rue%20Jangot" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + assert_raise ArgumentError, "The provided API key is invalid.", fn -> GoogleMaps.search("10 rue Jangot", api_key: "secret_key") end end test "returns a valid address from search" do - use_cassette "geospatial/google_maps/search" do - assert %Address{ - locality: "Lyon", - description: "10 Rue Jangot", - region: "Auvergne-Rhône-Alpes", - country: "France", - postal_code: "69007", - street: "10 Rue Jangot", - geom: %Geo.Point{ - coordinates: {4.8424032, 45.75164940000001}, - properties: %{}, - srid: 4326 - }, - origin_id: "gm:ChIJtW0QikTq9EcRLI4Vy6bRx0U" - } == - GoogleMaps.search("10 rue Jangot", - api_key: "toto" - ) - |> hd - end + data = + File.read!("test/fixtures/geospatial/google_maps/search.json") + |> Jason.decode!() + + data_2 = + File.read!("test/fixtures/geospatial/google_maps/search_2.json") + |> Jason.decode!() + + Mock + |> expect(:call, 3, fn + %{ + method: :get, + url: + "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=toto&language=en&address=10%20rue%20Jangot" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + + %{ + method: :get, + url: _url + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data_2}} + end) + + assert %Address{ + locality: "Lyon", + description: "10 Rue Jangot", + region: "Auvergne-Rhône-Alpes", + country: "France", + postal_code: "69007", + street: "10 Rue Jangot", + geom: %Geo.Point{ + coordinates: {4.8424032, 45.75164940000001}, + properties: %{}, + srid: 4326 + }, + origin_id: "gm:ChIJtW0QikTq9EcRLI4Vy6bRx0U" + } == + GoogleMaps.search("10 rue Jangot", + api_key: "toto" + ) + |> hd end test "returns a valid address from reverse geocode" do - use_cassette "geospatial/google_maps/geocode" do - assert %Address{ - locality: "Lyon", - description: "10bis Rue Jangot", - region: "Auvergne-Rhône-Alpes", - country: "France", - postal_code: "69007", - street: "10bis Rue Jangot", - geom: %Geo.Point{ - coordinates: {4.8424966, 45.751725}, - properties: %{}, - srid: 4326 - }, - origin_id: "gm:ChIJrW0QikTq9EcR96jk2OnO75w" - } == - GoogleMaps.geocode(4.842569, 45.751718, api_key: "toto") - |> hd - end + data = + File.read!("test/fixtures/geospatial/google_maps/geocode.json") + |> Jason.decode!() + + data_2 = + File.read!("test/fixtures/geospatial/google_maps/geocode_2.json") + |> Jason.decode!() + + Mock + |> expect(:call, 3, fn + %{ + method: :get, + url: + "https://maps.googleapis.com/maps/api/geocode/json?limit=10&key=toto&language=en&latlng=45.751718,4.842569&result_type=street_address" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + + %{ + method: :get, + url: _url + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data_2}} + end) + + assert %Address{ + locality: "Lyon", + description: "10bis Rue Jangot", + region: "Auvergne-Rhône-Alpes", + country: "France", + postal_code: "69007", + street: "10bis Rue Jangot", + geom: %Geo.Point{ + coordinates: {4.8424966, 45.751725}, + properties: %{}, + srid: 4326 + }, + origin_id: "gm:ChIJrW0QikTq9EcR96jk2OnO75w" + } == + GoogleMaps.geocode(4.842569, 45.751718, api_key: "toto") + |> hd end end end diff --git a/test/service/geospatial/map_quest_test.exs b/test/service/geospatial/map_quest_test.exs index ff25e8157..937fd6525 100644 --- a/test/service/geospatial/map_quest_test.exs +++ b/test/service/geospatial/map_quest_test.exs @@ -1,28 +1,11 @@ defmodule Mobilizon.Service.Geospatial.MapQuestTest do - use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney - use Mobilizon.DataCase - import Mock + import Mox alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.MapQuest - - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - - setup do - # Config.instance_user_agent/0 makes database calls so because of ownership connection - # we need to define it like this instead of a constant - # See https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html - {:ok, - httpoison_headers: [ - {"User-Agent", Config.instance_user_agent()} - ]} - end + alias Mobilizon.Service.HTTP.BaseClient.Mock describe "search address" do test "without API Key triggers an error" do @@ -31,75 +14,88 @@ defmodule Mobilizon.Service.Geospatial.MapQuestTest do end end - test "produces a valid search address with options", %{httpoison_headers: httpoison_headers} do - with_mock HTTPoison, - get: fn _url, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: "{\"info\": {\"statuscode\": 0}, \"results\": []}" - }} - end do - MapQuest.search("10 Rue Jangot", - limit: 5, - lang: "fr", - api_key: "toto" - ) - - assert_called( - HTTPoison.get( - "https://open.mapquestapi.com/geocoding/v1/address?key=toto&location=10%20Rue%20Jangot&maxResults=5", - httpoison_headers, - @http_options - ) - ) - end - end - test "triggers an error with an invalid API Key" do + Mock + |> expect(:call, fn + %{ + method: :get, + url: + "https://open.mapquestapi.com/geocoding/v1/address?key=secret_key&location=10%20rue%20Jangot&maxResults=10" + }, + _opts -> + {:ok, + %Tesla.Env{status: 403, body: "The AppKey submitted with this request is invalid."}} + end) + assert_raise ArgumentError, "The AppKey submitted with this request is invalid.", fn -> MapQuest.search("10 rue Jangot", api_key: "secret_key") end end test "returns a valid address from search" do - use_cassette "geospatial/map_quest/search" do - assert %Address{ - locality: "Lyon", - description: "10 Rue Jangot", - region: "Auvergne-Rhône-Alpes", - country: "FR", - postal_code: "69007", - street: "10 Rue Jangot", - geom: %Geo.Point{ - coordinates: {4.842566, 45.751714}, - properties: %{}, - srid: 4326 - } - } == - MapQuest.search("10 rue Jangot", api_key: "secret_key") - |> hd - end + data = + File.read!("test/fixtures/geospatial/map_quest/search.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{ + method: :get, + url: + "https://open.mapquestapi.com/geocoding/v1/address?key=secret_key&location=10%20rue%20Jangot&maxResults=10" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + assert %Address{ + locality: "Lyon", + description: "10 Rue Jangot", + region: "Auvergne-Rhône-Alpes", + country: "FR", + postal_code: "69007", + street: "10 Rue Jangot", + geom: %Geo.Point{ + coordinates: {4.842566, 45.751714}, + properties: %{}, + srid: 4326 + } + } == + MapQuest.search("10 rue Jangot", api_key: "secret_key") + |> hd end test "returns a valid address from reverse geocode" do - use_cassette "geospatial/map_quest/geocode" do - assert %Address{ - locality: "Lyon", - description: "10 Rue Jangot", - region: "Auvergne-Rhône-Alpes", - country: "FR", - postal_code: "69007", - street: "10 Rue Jangot", - geom: %Geo.Point{ - coordinates: {4.842569, 45.751718}, - properties: %{}, - srid: 4326 - } - } == - MapQuest.geocode(4.842569, 45.751718, api_key: "secret_key") - |> hd - end + data = + File.read!("test/fixtures/geospatial/map_quest/geocode.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{ + method: :get, + url: + "https://open.mapquestapi.com/geocoding/v1/reverse?key=secret_key&location=45.751718,4.842569&maxResults=10" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + assert %Address{ + locality: "Lyon", + description: "10 Rue Jangot", + region: "Auvergne-Rhône-Alpes", + country: "FR", + postal_code: "69007", + street: "10 Rue Jangot", + geom: %Geo.Point{ + coordinates: {4.842569, 45.751718}, + properties: %{}, + srid: 4326 + } + } == + MapQuest.geocode(4.842569, 45.751718, api_key: "secret_key") + |> hd end end end diff --git a/test/service/geospatial/nominatim_test.exs b/test/service/geospatial/nominatim_test.exs index cd01f760d..f3dedf651 100644 --- a/test/service/geospatial/nominatim_test.exs +++ b/test/service/geospatial/nominatim_test.exs @@ -1,91 +1,81 @@ defmodule Mobilizon.Service.Geospatial.NominatimTest do - use Mobilizon.DataCase, async: false - use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney - import Mock + use Mobilizon.DataCase + import Mox alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Nominatim - - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - - setup do - # Config.instance_user_agent/0 makes database calls so because of ownership connection - # we need to define it like this instead of a constant - # See https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html - {:ok, - httpoison_headers: [ - {"User-Agent", Config.instance_user_agent()} - ]} - end + alias Mobilizon.Service.HTTP.BaseClient.Mock describe "search address" do - test "produces a valid search address with options", %{httpoison_headers: httpoison_headers} do - with_mock HTTPoison, - get: fn _url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: "[]"}} - end do - Nominatim.search("10 Rue Jangot", - limit: 5, - lang: "fr" - ) - - assert_called( - HTTPoison.get( - "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20Rue%20Jangot&limit=5&accept-language=fr&addressdetails=1&namedetails=1", - httpoison_headers, - @http_options - ) - ) - end - end - test "returns a valid address from search" do - use_cassette "geospatial/nominatim/search" do - assert [ - %Address{ - locality: "Lyon", - description: "10 Rue Jangot", - region: "Auvergne-Rhône-Alpes", - country: "France", - postal_code: "69007", - street: "10 Rue Jangot", - geom: %Geo.Point{ - coordinates: {4.8425657, 45.7517141}, - properties: %{}, - srid: 4326 - }, - origin_id: "nominatim:3078260611", - type: "house" - } - ] == Nominatim.search("10 rue Jangot") - end + data = + File.read!("test/fixtures/geospatial/nominatim/search.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{ + method: :get, + url: + "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1&namedetails=1" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + assert [ + %Address{ + locality: "Lyon", + description: "10 Rue Jangot", + region: "Auvergne-Rhône-Alpes", + country: "France", + postal_code: "69007", + street: "10 Rue Jangot", + geom: %Geo.Point{ + coordinates: {4.8425657, 45.7517141}, + properties: %{}, + srid: 4326 + }, + origin_id: "nominatim:3078260611", + type: "house" + } + ] == Nominatim.search("10 rue Jangot") end test "returns a valid address from reverse geocode" do - use_cassette "geospatial/nominatim/geocode" do - assert [ - %Address{ - locality: "Lyon", - description: "10 Rue Jangot", - region: "Auvergne-Rhône-Alpes", - country: "France", - postal_code: "69007", - street: "10 Rue Jangot", - geom: %Geo.Point{ - coordinates: {4.8425657, 45.7517141}, - properties: %{}, - srid: 4326 - }, - origin_id: "nominatim:3078260611", - type: "house" - } - ] == - Nominatim.geocode(4.842569, 45.751718) - end + data = + File.read!("test/fixtures/geospatial/nominatim/geocode.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{ + method: :get, + url: + "https://nominatim.openstreetmap.org/reverse?format=geocodejson&lat=45.751718&lon=4.842569&accept-language=en&addressdetails=1&namedetails=1" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + assert [ + %Address{ + locality: "Lyon", + description: "10 Rue Jangot", + region: "Auvergne-Rhône-Alpes", + country: "France", + postal_code: "69007", + street: "10 Rue Jangot", + geom: %Geo.Point{ + coordinates: {4.8425657, 45.7517141}, + properties: %{}, + srid: 4326 + }, + origin_id: "nominatim:3078260611", + type: "house" + } + ] == + Nominatim.geocode(4.842569, 45.751718) end end end diff --git a/test/service/geospatial/photon_test.exs b/test/service/geospatial/photon_test.exs index 3d8d2185c..c5fa9fb9b 100644 --- a/test/service/geospatial/photon_test.exs +++ b/test/service/geospatial/photon_test.exs @@ -1,66 +1,41 @@ defmodule Mobilizon.Service.Geospatial.PhotonTest do - use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney - use Mobilizon.DataCase - import Mock + import Mox alias Mobilizon.Addresses.Address - alias Mobilizon.Config alias Mobilizon.Service.Geospatial.Photon - - @http_options [ - follow_redirect: true, - ssl: [{:versions, [:"tlsv1.2"]}] - ] - - setup do - # Config.instance_user_agent/0 makes database calls so because of ownership connection - # we need to define it like this instead of a constant - # See https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html - {:ok, - httpoison_headers: [ - {"User-Agent", Config.instance_user_agent()} - ]} - end + alias Mobilizon.Service.HTTP.BaseClient.Mock describe "search address" do - test "produces a valid search address with options", %{httpoison_headers: httpoison_headers} do - with_mock HTTPoison, - get: fn _url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: "{\"features\": []"}} - end do - Photon.search("10 Rue Jangot", - limit: 5, - lang: "fr" - ) - - assert_called( - HTTPoison.get( - "https://photon.komoot.de/api/?q=10%20Rue%20Jangot&lang=fr&limit=5", - httpoison_headers, - @http_options - ) - ) - end - end - test "returns a valid address from search" do - use_cassette "geospatial/photon/search" do - assert %Address{ - locality: "Lyon", - description: "10 Rue Jangot", - region: "Auvergne-Rhône-Alpes", - country: "France", - postal_code: "69007", - street: "10 Rue Jangot", - geom: %Geo.Point{ - coordinates: {4.8425657, 45.7517141}, - properties: %{}, - srid: 4326 - } - } == Photon.search("10 rue Jangot") |> hd - end + data = + File.read!("test/fixtures/geospatial/photon/search.json") + |> Jason.decode!() + + Mock + |> expect(:call, fn + %{ + method: :get, + url: "https://photon.komoot.de/api/?q=10%20rue%20Jangot&lang=en&limit=10" + }, + _opts -> + {:ok, %Tesla.Env{status: 200, body: data}} + end) + + assert %Address{ + locality: "Lyon", + description: "10 Rue Jangot", + region: "Auvergne-Rhône-Alpes", + country: "France", + postal_code: "69007", + street: "10 Rue Jangot", + geom: %Geo.Point{ + coordinates: {4.8425657, 45.7517141}, + properties: %{}, + srid: 4326 + } + } == Photon.search("10 rue Jangot") |> hd end # Photon returns something quite wrong, so no need to test this right now. diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 61b22b62c..79b301659 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -73,4 +73,7 @@ defmodule Mobilizon.DataCase do :ok end + + Mox.defmock(Mobilizon.Service.HTTP.ActivityPub.Mock, for: Tesla.Adapter) + Mox.defmock(Mobilizon.Service.HTTP.BaseClient.Mock, for: Tesla.Adapter) end diff --git a/test/support/factory.ex b/test/support/factory.ex index 7a9784fe7..84cf06dcb 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -118,12 +118,15 @@ defmodule Mobilizon.Factory do def comment_factory do uuid = Ecto.UUID.generate() - %Mobilizon.Conversations.Comment{ + %Mobilizon.Discussions.Comment{ text: "My Comment", actor: build(:actor), event: build(:event), uuid: uuid, mentions: [], + attributed_to: nil, + local: true, + deleted_at: nil, tags: build_list(3, :tag), in_reply_to_comment: nil, url: Routes.page_url(Endpoint, :comment, uuid) @@ -142,6 +145,7 @@ defmodule Mobilizon.Factory do begins_on: start, ends_on: Timex.shift(start, hours: 2), organizer_actor: actor, + attributed_to: nil, category: sequence("something"), physical_address: build(:address), visibility: :public, @@ -324,4 +328,50 @@ defmodule Mobilizon.Factory do value: sequence("value") } end + + def post_factory do + uuid = Ecto.UUID.generate() + + %Mobilizon.Posts.Post{ + body: "The HTMLbody for my Article", + title: "My Awesome article", + slug: "my-awesome-article-#{ShortUUID.encode!(uuid)}", + author: build(:actor), + attributed_to: build(:group), + id: uuid, + draft: false, + tags: build_list(3, :tag), + visibility: :public, + publish_at: DateTime.utc_now(), + url: Routes.page_url(Endpoint, :post, uuid) + } + end + + def tombstone_factory do + uuid = Ecto.UUID.generate() + + %Mobilizon.Tombstone{ + uri: "https://mobilizon.test/comments/#{uuid}", + actor: build(:actor) + } + end + + def discussion_factory do + uuid = Ecto.UUID.generate() + actor = build(:actor) + group = build(:group) + comment = build(:comment, actor: actor, attributed_to: group) + slug = "my-awesome-discussion-#{ShortUUID.encode!(uuid)}" + + %Mobilizon.Discussions.Discussion{ + title: "My Awesome discussion", + slug: slug, + creator: actor, + actor: group, + id: uuid, + last_comment: comment, + comments: [comment], + url: Routes.page_url(Endpoint, :discussion, group.preferred_username, slug) + } + end end diff --git a/test/web/controllers/activity_pub_controller_test.exs b/test/web/controllers/activity_pub_controller_test.exs index 8777cbae9..e5dcbf921 100644 --- a/test/web/controllers/activity_pub_controller_test.exs +++ b/test/web/controllers/activity_pub_controller_test.exs @@ -344,11 +344,12 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do Actors.create_group(%{ creator_actor_id: actor.id, preferred_username: "my_group", - visibility: :public + local: true }) result = conn + |> assign(:actor, actor) |> get(Actor.build_url(group.preferred_username, :members)) |> json_response(200) @@ -358,15 +359,21 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do assert hd(result["first"]["orderedItems"])["type"] == "Member" end - test "it returns no members for a private group", %{conn: conn} do + test "it returns no members when not a member of the group", %{conn: conn} do actor = insert(:actor) + actor2 = insert(:actor) assert {:ok, %Actor{} = group} = - Actors.create_group(%{creator_actor_id: actor.id, preferred_username: "my_group"}) + Actors.create_group(%{ + creator_actor_id: actor.id, + preferred_username: "my_group", + local: true + }) result = conn - |> get(Actor.build_url(actor.preferred_username, :members)) + |> assign(:actor, actor2) + |> get(Actor.build_url(group.preferred_username, :members)) |> json_response(200) assert result["first"]["orderedItems"] == [] @@ -379,7 +386,7 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do Actors.create_group(%{ creator_actor_id: actor.id, preferred_username: "my_group", - visibility: :public + local: true }) Enum.each(1..15, fn _ -> @@ -389,6 +396,7 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do result = conn + |> assign(:actor, actor) |> get(Actor.build_url(group.preferred_username, :members)) |> json_response(200) @@ -398,6 +406,7 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do result = conn + |> assign(:actor, actor) |> get(Actor.build_url(group.preferred_username, :members, page: 2)) |> json_response(200) @@ -411,7 +420,8 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do assert {:ok, %Actor{} = group} = Actors.create_group(%{ creator_actor_id: actor_group_admin.id, - preferred_username: "my_group" + preferred_username: "my_group", + local: true }) insert(:member, actor: actor_applicant, parent: group, role: :member) diff --git a/test/web/controllers/feed_controller_test.exs b/test/web/controllers/feed_controller_test.exs index e42471282..e6343a2ea 100644 --- a/test/web/controllers/feed_controller_test.exs +++ b/test/web/controllers/feed_controller_test.exs @@ -91,24 +91,26 @@ defmodule Mobilizon.Web.FeedControllerTest do describe "/@:preferred_username/feed/ics" do test "it returns an iCalendar representation of the actor's public events with an actor publicly visible", %{conn: conn} do - actor = insert(:actor, visibility: :public) + actor = insert(:actor) + group = insert(:group, visibility: :public) tag1 = insert(:tag, title: "iCalendar", slug: "icalendar") tag2 = insert(:tag, title: "Apple", slug: "apple") - event1 = insert(:event, organizer_actor: actor, tags: [tag1]) - event2 = insert(:event, organizer_actor: actor, tags: [tag1, tag2]) + event1 = insert(:event, organizer_actor: actor, attributed_to: group, tags: [tag1]) + event2 = insert(:event, organizer_actor: actor, attributed_to: group, tags: [tag1, tag2]) conn = conn |> get( Endpoint - |> Routes.feed_url(:actor, actor.preferred_username, "ics") + |> Routes.feed_url(:actor, group.preferred_username, "ics") |> URI.decode() ) - assert response(conn, 200) =~ "BEGIN:VCALENDAR" + assert res = response(conn, 200) + assert res =~ "BEGIN:VCALENDAR" assert response_content_type(conn, :calendar) =~ "charset=utf-8" - [entry1, entry2] = entries = ExIcal.parse(conn.resp_body) + [entry1, entry2] = entries = ExIcal.parse(res) Enum.each(entries, fn entry -> assert entry.summary in [event1.title, event2.title] @@ -120,7 +122,7 @@ defmodule Mobilizon.Web.FeedControllerTest do test "it returns a 404 page for the actor's public events iCal feed with an actor not publicly visible", %{conn: conn} do - actor = insert(:actor, visibility: :private) + actor = insert(:group, visibility: :private) tag1 = insert(:tag, title: "iCalendar", slug: "icalendar") tag2 = insert(:tag, title: "Apple", slug: "apple") insert(:event, organizer_actor: actor, tags: [tag1])