diff --git a/.gitignore b/.gitignore index 4f7f1202f..4c08c9957 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ erl_crash.dump .env.test /.env .env.2 +.env.1 /setup_db.psql diff --git a/config/config.exs b/config/config.exs index 8899ceaed..2780bfc92 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,7 +22,7 @@ config :mobilizon, :instance, repository: Mix.Project.config()[:source_url], allow_relay: true, # Federation is to be activated with Mobilizon 1.0.0-beta.2 - federating: false, + federating: true, remote_limit: 100_000, upload_limit: 10_000_000, avatar_upload_limit: 2_000_000, @@ -63,7 +63,7 @@ config :mobilizon, MobilizonWeb.Upload, config :mobilizon, MobilizonWeb.Uploaders.Local, uploads: "uploads" config :mobilizon, :media_proxy, - enabled: false, + enabled: true, proxy_opts: [ redirect_on_failure: false, max_body_length: 25 * 1_048_576, @@ -107,7 +107,9 @@ config :auto_linker, # TODO: Set to :no_scheme when it works properly validate_tld: true, class: false, - strip_prefix: false + strip_prefix: false, + new_window: true, + rel: "noopener noreferrer ugc" ] config :phoenix, :format_encoders, json: Jason, "activity-json": Jason @@ -120,6 +122,8 @@ config :ex_cldr, config :http_signatures, adapter: Mobilizon.Service.HTTPSignatures.Signature +config :mobilizon, :activitypub, sign_object_fetches: true + config :mobilizon, Mobilizon.Service.Geospatial.Nominatim, endpoint: System.get_env("GEOSPATIAL_NOMINATIM_ENDPOINT") || "https://nominatim.openstreetmap.org", @@ -155,7 +159,7 @@ config :mobilizon, :maps, config :mobilizon, Oban, repo: Mobilizon.Storage.Repo, prune: {:maxlen, 10_000}, - queues: [default: 10, search: 20] + queues: [default: 10, search: 20, background: 5] # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/docs/contribute/activity_pub.md b/docs/contribute/activity_pub.md new file mode 100644 index 000000000..47986abe9 --- /dev/null +++ b/docs/contribute/activity_pub.md @@ -0,0 +1,104 @@ +# Federation + +## ActivityPub + +Mobilizon uses [ActivityPub](http://activitypub.rocks/) to federate content between instances. It only supports the server-to-server part of [the ActivityPub spec](https://www.w3.org/TR/activitypub/). + +It implements the [HTTP signatures spec](https://tools.ietf.org/html/draft-cavage-http-signatures-12) for authentication of inbox deliveries, but doesn't implement Linked Data Signatures for forwarded payloads, and instead fetches content when needed. + +To match usernames to actors, Mobilizon uses [WebFinger](https://tools.ietf.org/html/rfc7033). + +## Instance subscriptions + +Instances subscribe to each other through an internal actor named `relay@instance.tld` that publishes (through `Announce`) every created content to it's followers. Each content creation share is saved so that updates and deletes are correctly sent to every + +## Activities + +Supported Activity | Supported Object +------------ | ------------- +`Accept` | `Follow`, `Join` +`Announce` | `Object` +`Create` | `Note`, `Event` +`Delete` | `Object` +`Flag` | `Object` +`Follow` | `Object` +`Reject` | `Follow`, `Join` +`Remove` | `Note`, `Event` +`Undo` | `Announce`, `Follow` +`Update` | `Object` + +## Extensions + +### Event + +The vocabulary for Event is based on [the Event object in ActivityStreams](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event), extended with : + +* the [Event Schema](https://schema.org/Event) from Schema.org +* some properties from [iCalendar](https://tools.ietf.org/html/rfc5545), such as `ical:status` (see [this issue](https://framagit.org/framasoft/mobilizon/issues/320)) + +The following properties are added. + +#### repliesModeration + +Disabling replies is [an ongoing issue with ActivityPub](https://github.com/w3c/activitypub/issues/319) so we use a temporary property. + +See [the corresponding issue](https://framagit.org/framasoft/mobilizon/issues/321). + +Accepted values: `allow_all`, `closed`, `moderated` (not used at the moment) + +Example: +```json +{ + "@context": [ + "...", + { + "mz": "https://joinmobilizon.org/ns#", + "repliesModerationOption": { + "@id": "mz:repliesModerationOption", + "@type": "mz:repliesModerationOptionType" + }, + "repliesModerationOptionType": { + "@id": "mz:repliesModerationOptionType", + "@type": "rdfs:Class" + } + } + ], + "...": "...", + "repliesModerationOption": "allow_all", + "type": "Event", + "url": "http://mobilizon1.com/events/8cf76e9f-c426-4912-9cd6-c7030b969611" +} +``` + + +#### joinMode + +Indicator of how new members may be able to join. + +See [the corresponding issue](https://framagit.org/framasoft/mobilizon/issues/321). + +Accepted values: `free`, `restricted`, `invite` (not used at the moment) + +Example: +```json +{ + "@context": [ + "...", + { + "mz": "https://joinmobilizon.org/ns#", + "joinMode": { + "@id": "mz:joinMode", + "@type": "mz:joinModeType" + }, + "joinModeType": { + "@id": "mz:joinModeType", + "@type": "rdfs:Class" + } + } + ], + "...": "...", + "joinMode": "restricted", + "type": "Event", + "url": "http://mobilizon1.com/events/8cf76e9f-c426-4912-9cd6-c7030b969611" +} +``` diff --git a/js/package.json b/js/package.json index 583f38f95..9feb0082e 100644 --- a/js/package.json +++ b/js/package.json @@ -12,15 +12,21 @@ "dev": "vue-cli-service build --watch", "styleguide": "vue-cli-service styleguidist", "styleguide:build": "vue-cli-service styleguidist:build", - "vue-i18n-extract": "vue-i18n-extract" + "vue-i18n-extract": "vue-i18n-extract", + "graphql:get-schema": "graphql get-schema", + "i18n-extract": "vue-i18n-extract report -v './src/**/*.?(ts|vue)' -l './src/i18n/en_US.json' -o output.json" }, "dependencies": { + "@absinthe/socket": "^0.2.1", + "@absinthe/socket-apollo-link": "^0.2.1", "@mdi/font": "^4.5.95", "apollo-absinthe-upload-link": "^1.5.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-link": "^1.2.11", "apollo-link-http": "^1.5.16", + "apollo-link-ws": "^1.0.19", + "apollo-utilities": "^1.3.2", "buefy": "^0.8.2", "graphql": "^14.5.8", "graphql-tag": "^2.10.1", @@ -30,6 +36,7 @@ "leaflet.locatecontrol": "^0.68.0", "lodash": "^4.17.11", "ngeohash": "^0.6.3", + "phoenix": "^1.4.11", "register-service-worker": "^1.6.2", "tippy.js": "4.3.5", "tiptap": "^1.26.0", diff --git a/js/src/components/Account/ParticipantCard.vue b/js/src/components/Account/ParticipantCard.vue index 59456fdba..cafac377b 100644 --- a/js/src/components/Account/ParticipantCard.vue +++ b/js/src/components/Account/ParticipantCard.vue @@ -26,7 +26,8 @@
{{ actorDisplayName }}
- @{{ participant.actor.preferredUsername }} + @{{ participant.actor.preferredUsername }}@{{ participant.actor.domain }} + @{{ participant.actor.preferredUsername }}
@@ -41,7 +42,7 @@ \ No newline at end of file diff --git a/js/src/components/Admin/Followings.vue b/js/src/components/Admin/Followings.vue new file mode 100644 index 000000000..97a58ddee --- /dev/null +++ b/js/src/components/Admin/Followings.vue @@ -0,0 +1,142 @@ + + \ No newline at end of file diff --git a/js/src/components/Comment/Comment.vue b/js/src/components/Comment/Comment.vue index ce31e8ff2..d9ee1207c 100644 --- a/js/src/components/Comment/Comment.vue +++ b/js/src/components/Comment/Comment.vue @@ -11,7 +11,8 @@
{{ comment.actor.name }} - @{{ comment.actor.preferredUsername }} + @{{ comment.actor.preferredUsername }}@{{ comment.actor.domain }} + @{{ comment.actor.preferredUsername }} {{ timeago(new Date(comment.updatedAt)) }} @@ -202,7 +203,7 @@ export default class Comment extends Vue { timeago(dateTime): String { if (this.timeAgoInstance != null) { - // @ts-ignore + // @ts-ignore return this.timeAgoInstance.format(dateTime); } return ''; @@ -213,7 +214,7 @@ export default class Comment extends Vue { } get commentFromOrganizer(): boolean { - return this.event.organizerActor !== undefined && this.comment.actor.id === this.event.organizerActor.id; + return this.event.organizerActor !== undefined && this.comment.actor && this.comment.actor.id === this.event.organizerActor.id; } get commentId(): String { @@ -230,6 +231,7 @@ export default class Comment extends Vue { title: this.$t('Report this comment'), comment: this.comment, onConfirm: this.reportComment, + outsideDomain: this.comment.actor.domain, }, }); } @@ -244,6 +246,7 @@ export default class Comment extends Vue { reportedId: this.comment.actor.id, commentsIds: [this.comment.id], content, + forward, }, }); this.$buefy.notification.open({ diff --git a/js/src/components/Comment/CommentTree.vue b/js/src/components/Comment/CommentTree.vue index df20bfec4..cf8b7d265 100644 --- a/js/src/components/Comment/CommentTree.vue +++ b/js/src/components/Comment/CommentTree.vue @@ -221,7 +221,7 @@ export default class CommentTree extends Vue { data: { thread: replies }, }); - // @ts-ignore + // @ts-ignore const parentCommentIndex = oldComments.findIndex(oldComment => oldComment.id === comment.originComment.id); const parentComment = oldComments[parentCommentIndex]; parentComment.replies = replies; diff --git a/js/src/components/Editor.vue b/js/src/components/Editor.vue index 3bc2ca9c6..8d21fe6a7 100644 --- a/js/src/components/Editor.vue +++ b/js/src/components/Editor.vue @@ -409,9 +409,9 @@ export default class EditorComponent extends Vue { } replyToComment(comment: IComment) { - console.log('called replyToComment', comment); const actorModel = new Actor(comment.actor); if (!this.editor) return; + console.log(this.editor.commands); this.editor.commands.mention({ id: actorModel.id, label: actorModel.usernameWithDomain().substring(1) }); this.editor.focus(); } diff --git a/js/src/components/Event/AddressAutoComplete.vue b/js/src/components/Event/AddressAutoComplete.vue index 2fd40f7d8..4de773b48 100644 --- a/js/src/components/Event/AddressAutoComplete.vue +++ b/js/src/components/Event/AddressAutoComplete.vue @@ -112,7 +112,7 @@ export default class AddressAutoComplete extends Vue { addressData: IAddress[] = []; selected: IAddress = new Address(); isFetching: boolean = false; - queryText: string = this.value && (new Address(this.value)).fullName || ''; + queryText: string = (this.value && (new Address(this.value)).fullName) || ''; addressModalActive: boolean = false; private gettingLocation: boolean = false; private location!: Position; @@ -164,6 +164,7 @@ export default class AddressAutoComplete extends Vue { @Watch('value') updateEditing() { + if (!(this.value && this.value.id)) return; this.selected = this.value; const address = new Address(this.selected); this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`; diff --git a/js/src/components/Event/ParticipationButton.vue b/js/src/components/Event/ParticipationButton.vue index 401fa7468..f48a1e2be 100644 --- a/js/src/components/Event/ParticipationButton.vue +++ b/js/src/components/Event/ParticipationButton.vue @@ -26,11 +26,11 @@ A button to set your participation
@@ -45,11 +45,11 @@ A button to set your participation
@@ -73,7 +73,7 @@ A button to set your participation - + @@ -84,12 +84,12 @@ A button to set your participation
- {{ $t('with {identity}', {identity: currentActor.preferredUsername }) }} + {{ $t('as {identity}', {identity: currentActor.preferredUsername }) }}
- + {{ $t('with another identity…')}} @@ -99,14 +99,32 @@ A button to set your participation \ No newline at end of file diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue index 209091676..fe1c0c484 100644 --- a/js/src/views/Event/Edit.vue +++ b/js/src/views/Event/Edit.vue @@ -29,7 +29,7 @@ - +
@@ -92,7 +92,7 @@
- + + + + + + {{ $t('Delete') }} @@ -74,24 +77,24 @@
-

+

{{ $t('No comment') }}

{{ report.event.title }}

-

+

- {{ $t('Edit') }} + + + + + + {{ $t('Delete') }}

@@ -101,17 +104,25 @@
-
+
Image
- {{ comment.actor.name }} @{{ comment.actor.preferredUsername }} + + {{ comment.actor.name }} @{{ comment.actor.preferredUsername }} + + {{ $t('Unknown actor') }}
-

+

+ {{ $t('Delete') }}
@@ -131,21 +142,23 @@ - {{ $t('Ajouter une note') }} + {{ $t('Add a note') }}
- \ No newline at end of file diff --git a/js/src/vue-apollo.ts b/js/src/vue-apollo.ts index 10a045cf1..f95e8f432 100644 --- a/js/src/vue-apollo.ts +++ b/js/src/vue-apollo.ts @@ -1,10 +1,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { ApolloLink, Observable } from 'apollo-link'; +import { ApolloLink, Observable, split } from 'apollo-link'; import { defaultDataIdFromObject, InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import { onError } from 'apollo-link-error'; import { createLink } from 'apollo-absinthe-upload-link'; -import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint'; +import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH, MOBILIZON_INSTANCE_HOST } from './api/_entrypoint'; import { ApolloClient } from 'apollo-client'; import { buildCurrentUserResolver } from '@/apollo/user'; import { isServerError } from '@/types/apollo'; @@ -13,13 +13,18 @@ import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from '@/constants'; import { logout, saveTokenData } from '@/utils/auth'; import { SnackbarProgrammatic as Snackbar } from 'buefy'; import { defaultError, errors, IError, refreshSuggestion } from '@/utils/errors'; +import { Socket as PhoenixSocket } from 'phoenix'; +import * as AbsintheSocket from '@absinthe/socket'; +import { createAbsintheSocketLink } from '@absinthe/socket-apollo-link'; +import { getMainDefinition } from 'apollo-utilities'; // Install the vue plugin Vue.use(VueApollo); -// Http endpoint +// Endpoints const httpServer = GRAPHQL_API_ENDPOINT || 'http://localhost:4000'; const httpEndpoint = GRAPHQL_API_FULL_PATH || `${httpServer}/api`; +const wsEndpoint = `ws${httpServer.substring(httpServer.indexOf(':'))}/graphql_socket`; const fragmentMatcher = new IntrospectionFragmentMatcher({ introspectionQueryResultData: { @@ -60,10 +65,6 @@ const authMiddleware = new ApolloLink((operation, forward) => { return null; }); -const uploadLink = createLink({ - uri: httpEndpoint, -}); - let refreshingTokenPromise: Promise | undefined; let alreadyRefreshedToken = false; const errorLink = onError(({ graphQLErrors, networkError, forward, operation }) => { @@ -126,9 +127,38 @@ const computeErrorMessage = (message) => { return error.suggestRefresh === false ? error.value : `${error.value}
${refreshSuggestion}`; }; -const link = authMiddleware +const uploadLink = createLink({ + uri: httpEndpoint, +}); + +const phoenixSocket = new PhoenixSocket(wsEndpoint, { + params: () => { + const token = localStorage.getItem(AUTH_ACCESS_TOKEN); + if (token) { + return { token }; + } + return {}; + + }, +}); + +const absintheSocket = AbsintheSocket.create(phoenixSocket); +const wsLink = createAbsintheSocketLink(absintheSocket); + +const link = split( + // split based on operation type + ({ query }) => { + const definition = getMainDefinition(query); + return definition.kind === 'OperationDefinition' && + definition.operation === 'subscription'; + }, + wsLink, + uploadLink, +); + +const fullLink = authMiddleware .concat(errorLink) - .concat(uploadLink); + .concat(link); const cache = new InMemoryCache({ fragmentMatcher, @@ -143,7 +173,7 @@ const cache = new InMemoryCache({ const apolloClient = new ApolloClient({ cache, - link, + link: fullLink, connectToDevTools: true, resolvers: buildCurrentUserResolver(cache), }); diff --git a/js/yarn.lock b/js/yarn.lock index a354fa3d3..22dea2b5e 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -2,6 +2,31 @@ # yarn lockfile v1 +"@absinthe/socket-apollo-link@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@absinthe/socket-apollo-link/-/socket-apollo-link-0.2.1.tgz#449c93109c903403b948abb8e911d9847cca9125" + integrity sha512-QxEazdjUXth+XMTAdlODZwS5h7fUAq9LEIH5O/EN0c/pS7Q3dFrTM1ZiP6n/0VdSEc+xBZyTisN63N2cPgE8ZQ== + dependencies: + "@absinthe/socket" "0.2.1" + "@babel/runtime" "7.2.0" + apollo-link "1.2.5" + core-js "2.6.0" + flow-static-land "0.2.8" + graphql "14.0.2" + zen-observable "0.8.11" + +"@absinthe/socket@0.2.1", "@absinthe/socket@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@absinthe/socket/-/socket-0.2.1.tgz#dd0d7bfc8e149f8376429c7fc2e87ac958578b91" + integrity sha512-rCuMRG4WndooGR+QfU5v+xL6U8YKEXFyvjqYt0qTHupAh+k+tpD6a5dlxcLO0g38p/hb1I12OzKvl+0G1XYCkA== + dependencies: + "@babel/runtime" "7.2.0" + "@jumpn/utils-array" "0.3.4" + "@jumpn/utils-composite" "0.7.0" + "@jumpn/utils-graphql" "0.6.0" + core-js "2.6.0" + zen-observable "0.8.11" + "@babel/code-frame@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" @@ -704,6 +729,13 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.2" +"@babel/runtime@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f" + integrity sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg== + dependencies: + regenerator-runtime "^0.12.0" + "@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.6.3": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.4.tgz#b23a856751e4bf099262f867767889c0e3fe175b" @@ -831,6 +863,35 @@ cssnano-preset-default "^4.0.0" postcss "^7.0.0" +"@jumpn/utils-array@0.3.4": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@jumpn/utils-array/-/utils-array-0.3.4.tgz#fb4310120108f659dab54075ef93abc56137de5e" + integrity sha1-+0MQEgEI9lnatUB175OrxWE33l4= + dependencies: + babel-polyfill "6.26.0" + babel-runtime "6.26.0" + flow-static-land "0.2.7" + +"@jumpn/utils-composite@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@jumpn/utils-composite/-/utils-composite-0.7.0.tgz#1979db00dd9465ebc33826adab17f1dd48e6fd0c" + integrity sha512-kamRVYJLNvjMrnKKeu2RSFQHLUO/IYFo05gLI7GQcCk063mJzsjCCfRycCievIBI+5Sg8C7A5gwRYxkBA5jY8w== + dependencies: + "@jumpn/utils-array" "0.3.4" + babel-polyfill "6.26.0" + babel-runtime "6.26.0" + fast-deep-equal "1.0.0" + flow-static-land "0.2.8" + +"@jumpn/utils-graphql@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@jumpn/utils-graphql/-/utils-graphql-0.6.0.tgz#9afd384c14e3f4caf68fa3ebeb3270218b18e931" + integrity sha512-I5OSEh8Ed4FdLIcUTYzWdpO9noQOoWptdgF8yOZ0xhDD7h7E9IgPYxfy36qbC6v9xlpGTwQMu3Wn8ulkinG/MQ== + dependencies: + "@babel/runtime" "7.2.0" + core-js "2.6.0" + graphql "14.0.2" + "@kbrandwijk/swagger-to-graphql@2.4.3": version "2.4.3" resolved "https://registry.yarnpkg.com/@kbrandwijk/swagger-to-graphql/-/swagger-to-graphql-2.4.3.tgz#7c0fb2410eb0b6b9cc81fad28cc20f9386153cf1" @@ -1900,6 +1961,22 @@ apollo-link-http@^1.3.2, apollo-link-http@^1.5.16: apollo-link-http-common "^0.2.15" tslib "^1.9.3" +apollo-link-ws@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/apollo-link-ws/-/apollo-link-ws-1.0.19.tgz#dfa871d4df883a8777c9556c872fc892e103daa5" + integrity sha512-mRXmeUkc55ixOdYRtfq5rq3o9sboKghKABKroDVhJnkdS56zthBEWMAD+phajujOUbqByxjok0te8ABqByBdeQ== + dependencies: + apollo-link "^1.2.13" + tslib "^1.9.3" + +apollo-link@1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.5.tgz#f54932d6b8f1412a35e088bc199a116bce3f1f16" + integrity sha512-GJHEE4B06oEB58mpRRwW6ISyvgX2aCqCLjpcE3M/6/4e+ZVeX7fRGpMJJDq2zZ8n7qWdrEuY315JfxzpsJmUhA== + dependencies: + apollo-utilities "^1.0.0" + zen-observable-ts "^0.8.12" + apollo-link@^1.0.0, apollo-link@^1.0.7, apollo-link@^1.2.11, apollo-link@^1.2.13: version "1.2.13" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" @@ -1910,7 +1987,7 @@ apollo-link@^1.0.0, apollo-link@^1.0.7, apollo-link@^1.2.11, apollo-link@^1.2.13 tslib "^1.9.3" zen-observable-ts "^0.8.20" -apollo-utilities@1.3.2, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: +apollo-utilities@1.3.2, apollo-utilities@^1.0.0, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== @@ -2205,7 +2282,16 @@ babel-plugin-transform-object-rest-spread@^6.26.0: babel-plugin-syntax-object-rest-spread "^6.8.0" babel-runtime "^6.26.0" -babel-runtime@^6.25.0, babel-runtime@^6.26.0: +babel-polyfill@6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" + integrity sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM= + dependencies: + babel-runtime "^6.26.0" + core-js "^2.5.0" + regenerator-runtime "^0.10.5" + +babel-runtime@6.26.0, babel-runtime@^6.25.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -3535,11 +3621,21 @@ core-js-pure@^3.0.0: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.4.2.tgz#ffd4ea4dc1f8517f75d4a929986a214629477417" integrity sha512-6+iSif/3zO0bSkhjVY9o4MTdv36X+rO6rqs/UxQ+uxBevmC4fsfwyQwFVdZXXONmLlKVLiXCG8PDvQ2Gn/iteA== +core-js@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.0.tgz#1e30793e9ee5782b307e37ffa22da0eacddd84d4" + integrity sha512-kLRC6ncVpuEW/1kwrOXYX6KQASCVtrh1gQr/UiaVgFlf9WE5Vp+lNe5+h3LuMr5PAucWnnEXwH0nQHRH/gpGtw== + core-js@^2.4.0, core-js@^2.5.3, core-js@^2.5.7, core-js@^2.6.10, core-js@^2.6.5: version "2.6.10" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f" integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA== +core-js@^2.5.0: + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== + core-js@^3.3.2, core-js@^3.3.5: version "3.4.2" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.4.2.tgz#ee2b1a60b50388d8ddcda8cdb44a92c7a9ea76df" @@ -5009,6 +5105,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +fast-deep-equal@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + integrity sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8= + fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -5263,6 +5364,16 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== +flow-static-land@0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/flow-static-land/-/flow-static-land-0.2.7.tgz#937f9dcb2780889a609155e5d1a55a993bc2ffb3" + integrity sha1-k3+dyyeAiJpgkVXl0aVamTvC/7M= + +flow-static-land@0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/flow-static-land/-/flow-static-land-0.2.8.tgz#49617e531396928bae6eb5d8ba32e7071637e5b9" + integrity sha512-pOZFExu2rbscCgcEo7nL7FNhBubMi18dn1Un4lm8LOmQkYhgsHLsrBGMWmuJXRWcYMrOC7I/bPsiqqVjdD3K1g== + flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -5939,6 +6050,13 @@ graphql@0.11.3: dependencies: iterall "^1.1.0" +graphql@14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650" + integrity sha512-gUC4YYsaiSJT1h40krG3J+USGlwhzNTXSb4IOZljn9ag5Tj+RkoXrWp+Kh7WyE3t1NCfab5kzCuxBIvOMERMXw== + dependencies: + iterall "^1.2.2" + graphql@^0.13.1: version "0.13.2" resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" @@ -9429,6 +9547,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +phoenix@^1.4.11: + version "1.4.11" + resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.4.11.tgz#3e70cd2461600ef3b0d4308e8fef7d2005fe000a" + integrity sha512-UkuqKB/+Uy9LNt15v1PpPeoLizcYwF4cFTi1wiMZ2TVX5+2Pj7MbRDFmoUFPc+Z1jsOE/TdQ6kzZohErWMiYlQ== + picomatch@^2.0.5: version "2.1.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.1.1.tgz#ecdfbea7704adb5fe6fb47f9866c4c0e15e905c5" @@ -10729,11 +10852,21 @@ regenerate@^1.4.0: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== +regenerator-runtime@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + integrity sha1-M2w+/BIgrc7dosn6tntaeVWjNlg= + regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== + regenerator-runtime@^0.13.2: version "0.13.3" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" @@ -14132,7 +14265,7 @@ z-schema@^3.18.2: optionalDependencies: commander "^2.7.1" -zen-observable-ts@^0.8.20: +zen-observable-ts@^0.8.12, zen-observable-ts@^0.8.20: version "0.8.20" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" integrity sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA== @@ -14140,6 +14273,11 @@ zen-observable-ts@^0.8.20: tslib "^1.9.3" zen-observable "^0.8.0" +zen-observable@0.8.11: + version "0.8.11" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199" + integrity sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ== + zen-observable@^0.8.0: version "0.8.15" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" diff --git a/lib/mix/tasks/mobilizon/relay.ex b/lib/mix/tasks/mobilizon/relay.ex index 989c7ea92..0e88ea603 100644 --- a/lib/mix/tasks/mobilizon/relay.ex +++ b/lib/mix/tasks/mobilizon/relay.ex @@ -30,7 +30,7 @@ defmodule Mix.Tasks.Mobilizon.Relay do Common.start_mobilizon() case Relay.follow(target) do - {:ok, _activity} -> + {:ok, _activity, _follow} -> # put this task to sleep to allow the genserver to push out the messages :timer.sleep(500) @@ -43,7 +43,7 @@ defmodule Mix.Tasks.Mobilizon.Relay do Common.start_mobilizon() case Relay.unfollow(target) do - {:ok, _activity} -> + {:ok, _activity, _follow} -> # put this task to sleep to allow the genserver to push out the messages :timer.sleep(500) diff --git a/lib/mobilizon.ex b/lib/mobilizon.ex index 6e18a6e26..49443f84d 100644 --- a/lib/mobilizon.ex +++ b/lib/mobilizon.ex @@ -37,6 +37,7 @@ defmodule Mobilizon do # supervisors Mobilizon.Storage.Repo, MobilizonWeb.Endpoint, + {Absinthe.Subscription, [MobilizonWeb.Endpoint]}, {Oban, Application.get_env(:mobilizon, Oban)}, # workers Guardian.DB.Token.SweeperServer, @@ -44,7 +45,8 @@ defmodule Mobilizon do cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1), cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1), cachex_spec(:statistics, 10, 60, 60), - cachex_spec(:activity_pub, 2500, 3, 15) + cachex_spec(:activity_pub, 2500, 3, 15), + internal_actor() ] Supervisor.start_link(children, strategy: :one_for_one, name: Mobilizon.Supervisor) @@ -88,4 +90,12 @@ defmodule Mobilizon do @spec fallback_options(function | nil) :: keyword defp fallback_options(nil), do: [] defp fallback_options(fallback), do: [fallback: fallback(default: fallback)] + + defp internal_actor() do + %{ + id: :internal_actor_init, + start: {Task, :start_link, [&Mobilizon.Service.ActivityPub.Relay.init/0]}, + restart: :temporary + } + end end diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index f5623b414..50de35138 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -7,9 +7,9 @@ defmodule Mobilizon.Actors.Actor do import Ecto.Changeset - alias Mobilizon.{Actors, Config, Crypto} + alias Mobilizon.{Actors, Config, Crypto, Share} alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member} - alias Mobilizon.Events.{Event, FeedToken} + alias Mobilizon.Events.{Event, FeedToken, Comment} alias Mobilizon.Media.File alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Users.User @@ -43,11 +43,14 @@ defmodule Mobilizon.Actors.Actor do followers: [Follower.t()], followings: [Follower.t()], organized_events: [Event.t()], + comments: [Comment.t()], feed_tokens: [FeedToken.t()], created_reports: [Report.t()], subject_reports: [Report.t()], report_notes: [Note.t()], mentions: [Mention.t()], + shares: [Share.t()], + owner_shares: [Share.t()], memberships: [t] } @@ -137,11 +140,14 @@ defmodule Mobilizon.Actors.Actor do has_many(:followers, Follower, foreign_key: :target_actor_id) has_many(:followings, Follower, foreign_key: :actor_id) has_many(:organized_events, Event, foreign_key: :organizer_actor_id) + has_many(:comments, Comment, foreign_key: :actor_id) has_many(:feed_tokens, FeedToken, foreign_key: :actor_id) has_many(:created_reports, Report, foreign_key: :reporter_id) has_many(:subject_reports, Report, foreign_key: :reported_id) has_many(:report_notes, Note, foreign_key: :moderator_id) has_many(:mentions, Mention) + has_many(:shares, Share, foreign_key: :actor_id) + has_many(:owner_shares, Share, foreign_key: :owner_actor_id) many_to_many(:memberships, __MODULE__, join_through: Member) timestamps() @@ -217,6 +223,19 @@ defmodule Mobilizon.Actors.Actor do |> validate_required(@update_required_attrs) end + @doc false + @spec delete_changeset(t) :: Ecto.Changeset.t() + def delete_changeset(%__MODULE__{} = actor) do + actor + |> change() + |> put_change(:name, nil) + |> put_change(:summary, nil) + |> put_change(:suspended, true) + |> put_change(:avatar, nil) + |> put_change(:banner, nil) + |> put_change(:user_id, nil) + end + @doc """ Changeset for person registration. """ diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index a928b85e7..05fc1c3de 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -12,6 +12,8 @@ defmodule Mobilizon.Actors do alias Mobilizon.{Crypto, Events} alias Mobilizon.Media.File alias Mobilizon.Storage.{Page, Repo} + alias Mobilizon.Service.Workers.BackgroundWorker + alias Mobilizon.Service.ActivityPub require Logger @@ -47,6 +49,7 @@ defmodule Mobilizon.Actors do @public_visibility [:public, :unlisted] @administrator_roles [:creator, :administrator] + @actor_preloads [:user, :organized_events, :comments] @doc """ Gets a single actor. @@ -224,16 +227,24 @@ defmodule Mobilizon.Actors do end end + def delete_actor(%Actor{} = actor) do + BackgroundWorker.enqueue("delete_actor", %{"actor_id" => actor.id}) + end + @doc """ Deletes an actor. """ - @spec delete_actor(Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} - def delete_actor(%Actor{domain: nil} = actor) do + @spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} + def perform(:delete_actor, %Actor{} = actor) do + actor = Repo.preload(actor, @actor_preloads) + transaction = Multi.new() - |> Multi.delete(:actor, actor) - |> Multi.run(:remove_banner, fn _, %{actor: %Actor{}} -> remove_banner(actor) end) - |> Multi.run(:remove_avatar, fn _, %{actor: %Actor{}} -> remove_avatar(actor) end) + |> Multi.run(:delete_organized_events, fn _, _ -> delete_actor_organized_events(actor) end) + |> Multi.run(:empty_comments, fn _, _ -> delete_actor_empty_comments(actor) end) + |> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end) + |> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end) + |> Multi.update(:actor, Actor.delete_changeset(actor)) |> Repo.transaction() case transaction do @@ -245,8 +256,6 @@ defmodule Mobilizon.Actors do end end - def delete_actor(%Actor{} = actor), do: Repo.delete(actor) - @doc """ Returns the list of actors. """ @@ -486,9 +495,9 @@ defmodule Mobilizon.Actors do |> Repo.insert() end - @spec get_or_create_actor_by_url(String.t(), String.t()) :: + @spec get_or_create_instance_actor_by_url(String.t(), String.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} - def get_or_create_actor_by_url(url, preferred_username \\ "relay") do + def get_or_create_instance_actor_by_url(url, preferred_username \\ "relay") do case get_actor_by_url(url) do {:ok, %Actor{} = actor} -> {:ok, actor} @@ -571,9 +580,12 @@ defmodule Mobilizon.Actors do """ @spec update_follower(Follower.t(), map) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} def update_follower(%Follower{} = follower, attrs) do - follower - |> Follower.changeset(attrs) - |> Repo.update() + with {:ok, %Follower{} = follower} <- + follower + |> Follower.changeset(attrs) + |> Repo.update() do + {:ok, Repo.preload(follower, [:actor, :target_actor])} + end end @doc """ @@ -597,10 +609,10 @@ defmodule Mobilizon.Actors do Returns the list of followers for an actor. If actor A and C both follow actor B, actor B's followers are A and C. """ - @spec list_followers_for_actor(Actor.t()) :: [Follower.t()] - def list_followers_for_actor(%Actor{id: actor_id}) do + @spec list_followers_actors_for_actor(Actor.t()) :: [Actor.t()] + def list_followers_actors_for_actor(%Actor{id: actor_id}) do actor_id - |> followers_for_actor_query() + |> follower_actors_for_actor_query() |> Repo.all() end @@ -610,18 +622,28 @@ defmodule Mobilizon.Actors do @spec list_external_followers_for_actor(Actor.t()) :: [Follower.t()] def list_external_followers_for_actor(%Actor{id: actor_id}) do actor_id - |> followers_for_actor_query() - |> filter_external() + |> list_external_follower_actors_for_actor_query() |> Repo.all() end + @doc """ + Returns the paginated list of external followers for an actor. + """ + @spec list_external_followers_for_actor_paginated(Actor.t(), integer | nil, integer | nil) :: + Page.t() + def list_external_followers_for_actor_paginated(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do + actor_id + |> list_external_followers_for_actor_query() + |> Page.build_page(page, limit) + end + @doc """ Build a page struct for followers of an actor. """ @spec build_followers_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t() def build_followers_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do actor_id - |> followers_for_actor_query() + |> follower_actors_for_actor_query() |> Page.build_page(page, limit) end @@ -632,17 +654,32 @@ defmodule Mobilizon.Actors do @spec list_followings_for_actor(Actor.t()) :: [Follower.t()] def list_followings_for_actor(%Actor{id: actor_id}) do actor_id - |> followings_for_actor_query() + |> followings_actors_for_actor_query() |> Repo.all() end + @doc """ + Returns the list of external followings for an actor. + """ + @spec list_external_followings_for_actor_paginated(Actor.t(), integer | nil, integer | nil) :: + Page.t() + def list_external_followings_for_actor_paginated( + %Actor{id: actor_id}, + page \\ nil, + limit \\ nil + ) do + actor_id + |> list_external_followings_for_actor_query() + |> Page.build_page(page, limit) + end + @doc """ Build a page struct for followings of an actor. """ @spec build_followings_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t() def build_followings_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do actor_id - |> followings_for_actor_query() + |> followings_actors_for_actor_query() |> Page.build_page(page, limit) end @@ -747,7 +784,7 @@ defmodule Mobilizon.Actors do defp actor_with_preload_query(actor_id) do from( a in Actor, - where: a.id == ^actor_id, + where: a.id == ^actor_id and not a.suspended, preload: [:organized_events, :followers, :followings] ) end @@ -885,12 +922,13 @@ defmodule Mobilizon.Actors do defp follower_by_followed_and_following_query(followed_id, follower_id) do from( f in Follower, - where: f.target_actor_id == ^followed_id and f.actor_id == ^follower_id + where: f.target_actor_id == ^followed_id and f.actor_id == ^follower_id, + preload: [:actor, :target_actor] ) end - @spec followers_for_actor_query(integer | String.t()) :: Ecto.Query.t() - defp followers_for_actor_query(actor_id) do + @spec follower_actors_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp follower_actors_for_actor_query(actor_id) do from( a in Actor, join: f in Follower, @@ -899,8 +937,18 @@ defmodule Mobilizon.Actors do ) end - @spec followings_for_actor_query(integer | String.t()) :: Ecto.Query.t() - defp followings_for_actor_query(actor_id) do + @spec follower_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp follower_for_actor_query(actor_id) do + from( + f in Follower, + join: a in Actor, + on: a.id == f.actor_id, + where: f.target_actor_id == ^actor_id + ) + end + + @spec followings_actors_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp followings_actors_for_actor_query(actor_id) do from( a in Actor, join: f in Follower, @@ -909,6 +957,38 @@ defmodule Mobilizon.Actors do ) end + @spec followings_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp followings_for_actor_query(actor_id) do + from( + f in Follower, + join: a in Actor, + on: a.id == f.target_actor_id, + where: f.actor_id == ^actor_id + ) + end + + @spec list_external_follower_actors_for_actor_query(integer) :: Ecto.Query.t() + defp list_external_follower_actors_for_actor_query(actor_id) do + actor_id + |> follower_actors_for_actor_query() + |> filter_external() + end + + @spec list_external_followers_for_actor_query(integer) :: Ecto.Query.t() + defp list_external_followers_for_actor_query(actor_id) do + actor_id + |> follower_for_actor_query() + |> filter_follower_actors_external() + end + + @spec list_external_followings_for_actor_query(integer) :: Ecto.Query.t() + defp list_external_followings_for_actor_query(actor_id) do + actor_id + |> followings_for_actor_query() + |> filter_follower_actors_external() + |> order_by(desc: :updated_at) + end + @spec filter_local(Ecto.Query.t()) :: Ecto.Query.t() defp filter_local(query) do from(a in query, where: is_nil(a.domain)) @@ -919,8 +999,16 @@ defmodule Mobilizon.Actors do from(a in query, where: not is_nil(a.domain)) end + @spec filter_follower_actors_external(Ecto.Query.t()) :: Ecto.Query.t() + defp filter_follower_actors_external(query) do + query + |> where([_f, a], not is_nil(a.domain)) + |> preload([f, a], [:target_actor, :actor]) + end + @spec filter_by_type(Ecto.Query.t(), ActorType.t()) :: Ecto.Query.t() - defp filter_by_type(query, type) when type in [:Person, :Group] do + defp filter_by_type(query, type) + when type in [:Person, :Group, :Application, :Service, :Organisation] do from(a in query, where: a.type == ^type) end @@ -943,4 +1031,36 @@ defmodule Mobilizon.Actors do @spec preload_followers(Actor.t(), boolean) :: Actor.t() 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 + res = + Enum.map(organized_events, fn event -> + event = + Repo.preload(event, [:organizer_actor, :participants, :picture, :mentions, :comments]) + + ActivityPub.delete(event, false) + end) + + if Enum.all?(res, fn {status, _, _} -> status == :ok end) do + {:ok, res} + else + {:error, res} + end + end + + defp delete_actor_empty_comments(%Actor{comments: comments}) do + res = + Enum.map(comments, fn comment -> + comment = + Repo.preload(comment, [:actor, :mentions, :event, :in_reply_to_comment, :origin_comment]) + + ActivityPub.delete(comment, false) + end) + + if Enum.all?(res, fn {status, _, _} -> status == :ok end) do + {:ok, res} + else + {:error, res} + end + end end diff --git a/lib/mobilizon/actors/follower.ex b/lib/mobilizon/actors/follower.ex index f38e00fd2..9b29e4c90 100644 --- a/lib/mobilizon/actors/follower.ex +++ b/lib/mobilizon/actors/follower.ex @@ -19,11 +19,15 @@ defmodule Mobilizon.Actors.Follower do @required_attrs [:url, :approved, :target_actor_id, :actor_id] @attrs @required_attrs + @timestamps_opts [type: :utc_datetime] + @primary_key {:id, :binary_id, autogenerate: true} schema "followers" do field(:approved, :boolean, default: false) field(:url, :string) + timestamps() + belongs_to(:target_actor, Actor) belongs_to(:actor, Actor) end diff --git a/lib/mobilizon/events/comment.ex b/lib/mobilizon/events/comment.ex index 9e2efd69d..8f18b0e7f 100644 --- a/lib/mobilizon/events/comment.ex +++ b/lib/mobilizon/events/comment.ex @@ -32,7 +32,6 @@ defmodule Mobilizon.Events.Comment do # When deleting an event we only nihilify everything @required_attrs [:url] @creation_required_attrs @required_attrs ++ [:text, :actor_id] - @deletion_required_attrs @required_attrs ++ [:deleted_at] @optional_attrs [ :text, :actor_id, @@ -81,11 +80,13 @@ defmodule Mobilizon.Events.Comment do |> validate_required(@creation_required_attrs) end - @spec delete_changeset(t, map) :: Ecto.Changeset.t() - def delete_changeset(%__MODULE__{} = comment, attrs) do + @spec delete_changeset(t) :: Ecto.Changeset.t() + def delete_changeset(%__MODULE__{} = comment) do comment - |> common_changeset(attrs) - |> validate_required(@deletion_required_attrs) + |> change() + |> put_change(:text, nil) + |> put_change(:actor_id, nil) + |> put_change(:deleted_at, DateTime.utc_now() |> DateTime.truncate(:second)) end @doc """ diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index 4dd35c7d1..603d858aa 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -13,6 +13,8 @@ defmodule Mobilizon.Events.Event do alias Mobilizon.Addresses + alias Mobilizon.Events + alias Mobilizon.Events.{ Comment, EventOptions, @@ -73,6 +75,7 @@ defmodule Mobilizon.Events.Event do :category, :status, :draft, + :local, :visibility, :join_options, :publish_at, @@ -190,13 +193,16 @@ defmodule Mobilizon.Events.Event do def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false} @spec put_tags(Changeset.t(), map) :: Changeset.t() - defp put_tags(%Changeset{} = changeset, %{tags: tags}), - do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1)) + defp put_tags(%Changeset{} = changeset, %{tags: tags}) do + put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1)) + end defp put_tags(%Changeset{} = changeset, _), do: changeset # We need a changeset instead of a raw struct because of slug which is generated in changeset - defp process_tag(%{id: _id} = tag), do: tag + defp process_tag(%{id: id} = _tag) do + Events.get_tag(id) + end defp process_tag(tag) do Tag.changeset(%Tag{}, tag) diff --git a/lib/mobilizon/events/event_participant_stats.ex b/lib/mobilizon/events/event_participant_stats.ex index 6e6878067..e5b8728dc 100644 --- a/lib/mobilizon/events/event_participant_stats.ex +++ b/lib/mobilizon/events/event_participant_stats.ex @@ -39,6 +39,21 @@ defmodule Mobilizon.Events.EventParticipantStats do @doc false @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = event_options, attrs) do - cast(event_options, attrs, @attrs) + event_options + |> cast(attrs, @attrs) + |> validate_stats() + end + + defp validate_stats(%Ecto.Changeset{} = changeset) do + changeset + |> validate_number(:not_approved, greater_than_or_equal_to: 0) + |> validate_number(:rejected, greater_than_or_equal_to: 0) + |> validate_number(:participant, greater_than_or_equal_to: 0) + |> validate_number(:moderator, greater_than_or_equal_to: 0) + |> validate_number(:administrator, greater_than_or_equal_to: 0) + |> validate_number(:creator, greater_than_or_equal_to: 0) + + # TODO: Replace me with something like the following + # Enum.reduce(@attrs, fn key, changeset -> validate_number(changeset, key, greater_than_or_equal_to: 0) end) end end diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 9e17d7826..0427c638c 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -97,6 +97,7 @@ defmodule Mobilizon.Events do @comment_preloads [ :actor, + :event, :attributed_to, :in_reply_to_comment, :origin_comment, @@ -722,6 +723,13 @@ defmodule Mobilizon.Events do |> Repo.all() end + @spec list_actors_participants_for_event(String.t()) :: [Actor.t()] + def list_actors_participants_for_event(id) do + id + |> list_participant_actors_for_event_query + |> Repo.all() + end + @doc """ Returns the list of participations for an actor. @@ -864,30 +872,15 @@ defmodule Mobilizon.Events do |> Multi.run(:update_event_participation_stats, fn _repo, %{ participant: - %Participant{ - role: role, - event_id: event_id - } = _participant + %Participant{role: new_role} = + participant } -> - with {:update_event_participation_stats, true} <- - {:update_event_participation_stats, update_event_participation_stats}, - {:ok, %Event{} = event} <- get_event(event_id), - %EventParticipantStats{} = participant_stats <- - Map.get(event, :participant_stats), - %EventParticipantStats{} = participant_stats <- - Map.update(participant_stats, role, 0, &(&1 + 1)), - {:ok, %Event{} = event} <- - event - |> Event.update_changeset(%{ - participant_stats: Map.from_struct(participant_stats) - }) - |> Repo.update() do - {:ok, event} - else - {:update_event_participation_stats, false} -> {:ok, nil} - {:error, :event_not_found} -> {:error, :event_not_found} - err -> {:error, err} - end + update_participant_stats( + participant, + nil, + new_role, + update_event_participation_stats + ) end) |> Repo.transaction() do {:ok, Repo.preload(participant, [:event, :actor])} @@ -899,10 +892,21 @@ defmodule Mobilizon.Events do """ @spec update_participant(Participant.t(), map) :: {:ok, Participant.t()} | {:error, Changeset.t()} - def update_participant(%Participant{} = participant, attrs) do - participant - |> Participant.changeset(attrs) - |> Repo.update() + def update_participant(%Participant{role: old_role} = participant, attrs) do + with {:ok, %{participant: %Participant{} = participant}} <- + Multi.new() + |> Multi.update(:participant, Participant.changeset(participant, attrs)) + |> Multi.run(:update_event_participation_stats, fn _repo, + %{ + participant: + %Participant{role: new_role} = + participant + } -> + update_participant_stats(participant, old_role, new_role) + end) + |> Repo.transaction() do + {:ok, Repo.preload(participant, [:event, :actor])} + end end @doc """ @@ -910,7 +914,71 @@ defmodule Mobilizon.Events do """ @spec delete_participant(Participant.t()) :: {:ok, Participant.t()} | {:error, Changeset.t()} - def delete_participant(%Participant{} = participant), do: Repo.delete(participant) + def delete_participant(%Participant{role: old_role} = participant) do + with {:ok, %{participant: %Participant{} = participant}} <- + Multi.new() + |> Multi.delete(:participant, participant) + |> Multi.run(:update_event_participation_stats, fn _repo, + %{ + participant: + %Participant{} = participant + } -> + update_participant_stats(participant, old_role, nil) + end) + |> Repo.transaction() do + {:ok, participant} + end + end + + defp update_participant_stats( + %Participant{ + event_id: event_id + } = _participant, + old_role, + new_role, + update_event_participation_stats \\ true + ) do + with {:update_event_participation_stats, true} <- + {:update_event_participation_stats, update_event_participation_stats}, + {:ok, %Event{} = event} <- get_event(event_id), + %EventParticipantStats{} = participant_stats <- + Map.get(event, :participant_stats), + %EventParticipantStats{} = participant_stats <- + do_update_participant_stats(participant_stats, old_role, new_role), + {:ok, %Event{} = event} <- + event + |> Event.update_changeset(%{ + participant_stats: Map.from_struct(participant_stats) + }) + |> Repo.update() do + {:ok, event} + else + {:update_event_participation_stats, false} -> + {:ok, nil} + + {:error, :event_not_found} -> + {:error, :event_not_found} + + err -> + {:error, err} + end + end + + defp do_update_participant_stats(participant_stats, old_role, new_role) do + participant_stats + |> decrease_participant_stats(old_role) + |> increase_participant_stats(new_role) + end + + defp increase_participant_stats(participant_stats, nil), do: participant_stats + + defp increase_participant_stats(participant_stats, role), + do: Map.update(participant_stats, role, 0, &(&1 + 1)) + + defp decrease_participant_stats(participant_stats, nil), do: participant_stats + + defp decrease_participant_stats(participant_stats, role), + do: Map.update(participant_stats, role, 0, &(&1 - 1)) @doc """ Gets a single session. @@ -1170,11 +1238,7 @@ defmodule Mobilizon.Events do @spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Changeset.t()} def delete_comment(%Comment{} = comment) do comment - |> Comment.delete_changeset(%{ - text: nil, - actor_id: nil, - deleted_at: DateTime.utc_now() - }) + |> Comment.delete_changeset() |> Repo.update() end @@ -1561,14 +1625,22 @@ defmodule Mobilizon.Events do defp list_participants_for_event_query(event_id) do from( p in Participant, - join: e in Event, - on: p.event_id == e.id, - where: e.id == ^event_id, + where: p.event_id == ^event_id, preload: [:actor] ) end - @spec list_participants_for_event_query(String.t()) :: Ecto.Query.t() + @spec list_participant_actors_for_event_query(String.t()) :: Ecto.Query.t() + defp list_participant_actors_for_event_query(event_id) do + from( + a in Actor, + join: p in Participant, + on: p.actor_id == a.id, + where: p.event_id == ^event_id + ) + end + + @spec list_local_emails_user_participants_for_event_query(String.t()) :: Ecto.Query.t() def list_local_emails_user_participants_for_event_query(event_id) do Participant |> join(:inner, [p], a in Actor, on: p.actor_id == a.id and is_nil(a.domain)) diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex index c3d851e18..a5bd70800 100644 --- a/lib/mobilizon/events/participant.ex +++ b/lib/mobilizon/events/participant.ex @@ -57,6 +57,7 @@ defmodule Mobilizon.Events.Participant do |> cast(attrs, @attrs) |> ensure_url() |> validate_required(@required_attrs) + |> unique_constraint(:actor_id, name: :participants_event_id_actor_id_index) end # If there's a blank URL that's because we're doing the first insert diff --git a/lib/mobilizon/share.ex b/lib/mobilizon/share.ex new file mode 100644 index 000000000..a6408fd91 --- /dev/null +++ b/lib/mobilizon/share.ex @@ -0,0 +1,75 @@ +defmodule Mobilizon.Share do + @moduledoc """ + Holds the list of shares made to external actors + """ + + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + alias Mobilizon.Storage.Repo + alias Mobilizon.Actors.Actor + + @type t :: %__MODULE__{ + uri: String.t(), + actor: Actor.t() + } + + @required_attrs [:uri, :actor_id, :owner_actor_id] + @optional_attrs [] + @attrs @required_attrs ++ @optional_attrs + + schema "shares" do + field(:uri, :string) + + belongs_to(:actor, Actor) + belongs_to(:owner_actor, Actor) + timestamps() + end + + @doc false + def changeset(share, attrs) do + share + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:actor_id) + |> unique_constraint(:uri, name: :shares_uri_actor_id_index) + end + + @spec create(String.t(), integer(), integer()) :: + {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def create(uri, actor_id, owner_actor_id) do + %__MODULE__{} + |> changeset(%{actor_id: actor_id, owner_actor_id: owner_actor_id, uri: uri}) + |> Repo.insert(on_conflict: :nothing) + end + + @spec get(String.t(), integer()) :: Ecto.Schema.t() | nil + def get(uri, actor_id) do + __MODULE__ + |> where(actor_id: ^actor_id, uri: ^uri) + |> Repo.one() + end + + @spec get_actors_by_share_uri(String.t()) :: [Ecto.Schema.t()] + def get_actors_by_share_uri(uri) do + Actor + |> join(:inner, [a], s in __MODULE__, on: s.actor_id == a.id) + |> where([_a, s], s.uri == ^uri) + |> Repo.all() + end + + @spec get_actors_by_owner_actor_id(integer()) :: [Ecto.Schema.t()] + def get_actors_by_owner_actor_id(actor_id) do + Actor + |> join(:inner, [a], s in __MODULE__, on: s.actor_id == a.id) + |> where([_a, s], s.owner_actor_id == ^actor_id) + |> Repo.all() + end + + @spec delete_all_by_uri(String.t()) :: {integer(), nil | [term()]} + def delete_all_by_uri(uri) do + __MODULE__ + |> where(uri: ^uri) + |> Repo.delete_all() + end +end diff --git a/lib/mobilizon/tombstone.ex b/lib/mobilizon/tombstone.ex index d0160fe40..e409fa84b 100644 --- a/lib/mobilizon/tombstone.ex +++ b/lib/mobilizon/tombstone.ex @@ -28,7 +28,7 @@ defmodule Mobilizon.Tombstone do def changeset(%__MODULE__{} = tombstone, attrs) do tombstone |> cast(attrs, @attrs) - |> validate_required(@attrs) + |> validate_required(@required_attrs) end @spec create_tombstone(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} diff --git a/lib/mobilizon_web/api/follows.ex b/lib/mobilizon_web/api/follows.ex index 0c9539660..c86f6d3f4 100644 --- a/lib/mobilizon_web/api/follows.ex +++ b/lib/mobilizon_web/api/follows.ex @@ -12,8 +12,8 @@ defmodule MobilizonWeb.API.Follows do def follow(%Actor{} = follower, %Actor{} = followed) do case ActivityPub.follow(follower, followed) do - {:ok, activity, _} -> - {:ok, activity} + {:ok, activity, follow} -> + {:ok, activity, follow} e -> Logger.warn("Error while following actor: #{inspect(e)}") @@ -23,8 +23,8 @@ defmodule MobilizonWeb.API.Follows do def unfollow(%Actor{} = follower, %Actor{} = followed) do case ActivityPub.unfollow(follower, followed) do - {:ok, activity, _} -> - {:ok, activity} + {:ok, activity, follow} -> + {:ok, activity, follow} e -> Logger.warn("Error while unfollowing actor: #{inspect(e)}") @@ -33,15 +33,35 @@ defmodule MobilizonWeb.API.Follows do end def accept(%Actor{} = follower, %Actor{} = followed) do + Logger.debug("We're trying to accept a follow") + with %Follower{approved: false} = follow <- Actors.is_following(follower, followed), - {:ok, %Activity{} = activity, %Follower{approved: true}} <- + {:ok, %Activity{} = activity, %Follower{approved: true} = follow} <- ActivityPub.accept( :follow, follow, - %{approved: true} + true ) do - {:ok, activity} + {:ok, activity, follow} + else + %Follower{approved: true} -> + {:error, "Follow already accepted"} + end + end + + def reject(%Actor{} = follower, %Actor{} = followed) do + Logger.debug("We're trying to reject a follow") + + with %Follower{} = follow <- + Actors.is_following(follower, followed), + {:ok, %Activity{} = activity, %Follower{} = follow} <- + ActivityPub.reject( + :follow, + follow, + true + ) do + {:ok, activity, follow} else %Follower{approved: true} -> {:error, "Follow already accepted"} diff --git a/lib/mobilizon_web/api/groups.ex b/lib/mobilizon_web/api/groups.ex index 581d6630b..d19329e71 100644 --- a/lib/mobilizon_web/api/groups.ex +++ b/lib/mobilizon_web/api/groups.ex @@ -17,7 +17,8 @@ defmodule MobilizonWeb.API.Groups do args |> Map.get(:preferred_username) |> HtmlSanitizeEx.strip_tags() |> String.trim(), {:existing_group, nil} <- {:existing_group, Actors.get_local_group_by_title(preferred_username)}, - {:ok, %Activity{} = activity, %Actor{} = group} <- ActivityPub.create(:group, args, true) do + {:ok, %Activity{} = activity, %Actor{} = group} <- + ActivityPub.create(:group, args, true, %{"actor" => args.creator_actor.url}) do {:ok, activity, group} else {:existing_group, _} -> diff --git a/lib/mobilizon_web/api/participations.ex b/lib/mobilizon_web/api/participations.ex index 022319060..3bd6a348e 100644 --- a/lib/mobilizon_web/api/participations.ex +++ b/lib/mobilizon_web/api/participations.ex @@ -4,7 +4,6 @@ defmodule MobilizonWeb.API.Participations do """ alias Mobilizon.Actors.Actor - alias Mobilizon.Events alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Service.ActivityPub alias MobilizonWeb.Email.Participation @@ -36,16 +35,13 @@ defmodule MobilizonWeb.API.Participations do %Participant{} = participation, %Actor{} = moderator ) do - with {:ok, activity, _} <- + with {:ok, activity, %Participant{role: :participant} = participation} <- ActivityPub.accept( :join, participation, - %{role: :participant}, true, - %{"to" => [moderator.url]} + %{"actor" => moderator.url} ), - {:ok, %Participant{role: :participant} = participation} <- - Events.update_participant(participation, %{"role" => :participant}), :ok <- Participation.send_emails_to_local_user(participation) do {:ok, activity, participation} end @@ -55,17 +51,12 @@ defmodule MobilizonWeb.API.Participations do %Participant{} = participation, %Actor{} = moderator ) do - with {:ok, activity, _} <- + with {:ok, activity, %Participant{role: :rejected} = participation} <- ActivityPub.reject( - %{ - to: [participation.actor.url], - actor: moderator.url, - object: participation.url - }, - "#{MobilizonWeb.Endpoint.url()}/reject/join/#{participation.id}" + :join, + participation, + %{"actor" => moderator.url} ), - {:ok, %Participant{role: :rejected} = participation} <- - Events.update_participant(participation, %{"role" => :rejected}), :ok <- Participation.send_emails_to_local_user(participation) do {:ok, activity, participation} end diff --git a/lib/mobilizon_web/api/reports.ex b/lib/mobilizon_web/api/reports.ex index 0b6dc89ff..278ef828d 100644 --- a/lib/mobilizon_web/api/reports.ex +++ b/lib/mobilizon_web/api/reports.ex @@ -17,7 +17,7 @@ defmodule MobilizonWeb.API.Reports do Create a report/flag on an actor, and optionally on an event or on comments. """ def report(args) do - case {:make_activity, ActivityPub.flag(args, Map.get(args, :local, false) == false)} do + case {:make_activity, ActivityPub.flag(args, Map.get(args, :forward, false) == true)} do {:make_activity, {:ok, %Activity{} = activity, %Report{} = report}} -> {:ok, activity, report} diff --git a/lib/mobilizon_web/cache/activity_pub.ex b/lib/mobilizon_web/cache/activity_pub.ex index d7d5c87d5..14c768256 100644 --- a/lib/mobilizon_web/cache/activity_pub.ex +++ b/lib/mobilizon_web/cache/activity_pub.ex @@ -3,10 +3,12 @@ defmodule MobilizonWeb.Cache.ActivityPub do The ActivityPub related functions. """ - alias Mobilizon.{Actors, Events, Service} + alias Mobilizon.{Actors, Events, Service, Tombstone} alias Mobilizon.Actors.Actor alias Mobilizon.Events.{Comment, Event} alias Service.ActivityPub + alias MobilizonWeb.Router.Helpers, as: Routes + alias MobilizonWeb.Endpoint @cache :activity_pub @@ -39,7 +41,12 @@ defmodule MobilizonWeb.Cache.ActivityPub do {:commit, event} nil -> - {:ignore, nil} + with url <- Routes.page_url(Endpoint, :event, uuid), + %Tombstone{} = tomstone <- Tombstone.find_tombstone(url) do + tomstone + else + _ -> {:ignore, nil} + end end end) end diff --git a/lib/mobilizon_web/channels/graphql_socket.ex b/lib/mobilizon_web/channels/graphql_socket.ex new file mode 100644 index 000000000..72372a663 --- /dev/null +++ b/lib/mobilizon_web/channels/graphql_socket.ex @@ -0,0 +1,28 @@ +defmodule MobilizonWeb.GraphQLSocket do + use Phoenix.Socket + + use Absinthe.Phoenix.Socket, + schema: MobilizonWeb.Schema + + alias Mobilizon.Users.User + + def connect(%{"token" => token}, socket) do + with {:ok, authed_socket} <- + Guardian.Phoenix.Socket.authenticate(socket, MobilizonWeb.Guardian, token), + %User{} = user <- Guardian.Phoenix.Socket.current_resource(authed_socket) do + authed_socket = + Absinthe.Phoenix.Socket.put_options(socket, + context: %{ + current_user: user + } + ) + + {:ok, authed_socket} + else + {:error, _} -> + :error + end + end + + def id(_socket), do: nil +end diff --git a/lib/mobilizon_web/controllers/activity_pub_controller.ex b/lib/mobilizon_web/controllers/activity_pub_controller.ex index 0c749c963..51c5ecb13 100644 --- a/lib/mobilizon_web/controllers/activity_pub_controller.ex +++ b/lib/mobilizon_web/controllers/activity_pub_controller.ex @@ -17,6 +17,7 @@ defmodule MobilizonWeb.ActivityPubController do action_fallback(:errors) + plug(MobilizonWeb.Plugs.Federating when action in [:inbox, :relay]) plug(:relay_active? when action in [:relay]) def relay_active?(conn, _) do @@ -114,7 +115,7 @@ defmodule MobilizonWeb.ActivityPubController do end def relay(conn, _params) do - with {:commit, %Actor{} = actor} <- Cache.get_relay() do + with {status, %Actor{} = actor} when status in [:commit, :ok] <- Cache.get_relay() do conn |> put_resp_header("content-type", "application/activity+json") |> json(ActorView.render("actor.json", %{actor: actor})) diff --git a/lib/mobilizon_web/controllers/page_controller.ex b/lib/mobilizon_web/controllers/page_controller.ex index 6e7f5fe89..a79f94b6a 100644 --- a/lib/mobilizon_web/controllers/page_controller.ex +++ b/lib/mobilizon_web/controllers/page_controller.ex @@ -28,13 +28,22 @@ defmodule MobilizonWeb.PageController do defp render_or_error(conn, check_fn, status, object_type, object) do if check_fn.(status, object) do - render(conn, object_type, object: object) + case object do + %Mobilizon.Tombstone{} -> + conn + |> put_status(:gone) + |> render(object_type, object: object) + + _ -> + render(conn, object_type, object: object) + end else {:error, :not_found} end end defp is_visible?(%{visibility: v}), do: v in [:public, :unlisted] + defp is_visible?(%Mobilizon.Tombstone{}), do: true defp ok_status?(status), do: status in [:ok, :commit] defp ok_status?(status, _), do: ok_status?(status) diff --git a/lib/mobilizon_web/controllers/web_finger_controller.ex b/lib/mobilizon_web/controllers/web_finger_controller.ex index a74bc41b3..dca47c231 100644 --- a/lib/mobilizon_web/controllers/web_finger_controller.ex +++ b/lib/mobilizon_web/controllers/web_finger_controller.ex @@ -9,6 +9,7 @@ defmodule MobilizonWeb.WebFingerController do """ use MobilizonWeb, :controller + plug(MobilizonWeb.Plugs.Federating) alias Mobilizon.Service.WebFinger @doc """ diff --git a/lib/mobilizon_web/endpoint.ex b/lib/mobilizon_web/endpoint.ex index 1ff261e7a..23fb15a15 100644 --- a/lib/mobilizon_web/endpoint.ex +++ b/lib/mobilizon_web/endpoint.ex @@ -3,6 +3,7 @@ defmodule MobilizonWeb.Endpoint do Endpoint for Mobilizon app """ use Phoenix.Endpoint, otp_app: :mobilizon + use Absinthe.Phoenix.Endpoint # For e2e tests if Application.get_env(:mobilizon, :sql_sandbox) do @@ -13,6 +14,11 @@ defmodule MobilizonWeb.Endpoint do ) end + socket("/graphql_socket", MobilizonWeb.GraphQLSocket, + websocket: true, + longpoll: false + ) + plug(MobilizonWeb.Plugs.UploadedMedia) # Serve at "/" the static files from "priv/static" directory. diff --git a/lib/mobilizon_web/http_signature.ex b/lib/mobilizon_web/http_signature.ex index f6c1f2dba..5e0580d16 100644 --- a/lib/mobilizon_web/http_signature.ex +++ b/lib/mobilizon_web/http_signature.ex @@ -22,30 +22,36 @@ defmodule MobilizonWeb.HTTPSignaturePlug do end def call(conn, _opts) do - [signature | _] = get_req_header(conn, "signature") + case get_req_header(conn, "signature") do + [signature | _] -> + if signature do + # set (request-target) header to the appropriate value + # we also replace the digest header with the one we computed + conn = + conn + |> put_req_header( + "(request-target)", + String.downcase("#{conn.method}") <> " #{conn.request_path}" + ) - if signature do - # set (request-target) header to the appropriate value - # we also replace the digest header with the one we computed - conn = - conn - |> put_req_header( - "(request-target)", - String.downcase("#{conn.method}") <> " #{conn.request_path}" - ) + conn = + if conn.assigns[:digest] do + conn + |> put_req_header("digest", conn.assigns[:digest]) + else + conn + end - conn = - if conn.assigns[:digest] do - conn - |> put_req_header("digest", conn.assigns[:digest]) + signature_valid = HTTPSignatures.validate_conn(conn) + Logger.debug("Is signature valid ? #{inspect(signature_valid)}") + assign(conn, :valid_signature, signature_valid) else + Logger.debug("No signature header!") conn end - assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) - else - Logger.debug("No signature header!") - conn + _ -> + conn end end end diff --git a/lib/mobilizon_web/plugs/federating.ex b/lib/mobilizon_web/plugs/federating.ex new file mode 100644 index 000000000..282c8ab2c --- /dev/null +++ b/lib/mobilizon_web/plugs/federating.ex @@ -0,0 +1,27 @@ +# Portions of this file are derived from Pleroma: +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule MobilizonWeb.Plugs.Federating do + @moduledoc """ + Restrict ActivityPub routes when not federating + """ + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _opts) do + if Mobilizon.Config.get([:instance, :federating]) do + conn + else + conn + |> put_status(404) + |> Phoenix.Controller.put_view(MobilizonWeb.ErrorView) + |> Phoenix.Controller.render("404.json") + |> halt() + end + end +end diff --git a/lib/mobilizon_web/plugs/mapped_signature_to_identity.ex b/lib/mobilizon_web/plugs/mapped_signature_to_identity.ex new file mode 100644 index 000000000..361325848 --- /dev/null +++ b/lib/mobilizon_web/plugs/mapped_signature_to_identity.ex @@ -0,0 +1,79 @@ +# Portions of this file are derived from Pleroma: +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule MobilizonWeb.Plugs.MappedSignatureToIdentity do + @moduledoc """ + Get actor identity from Signature when handing fetches + """ + alias Mobilizon.Service.HTTPSignatures.Signature + alias Mobilizon.Actors.Actor + alias Mobilizon.Service.ActivityPub.Utils + alias Mobilizon.Service.ActivityPub + + import Plug.Conn + require Logger + + def init(options), do: options + + @spec key_id_from_conn(Plug.Conn.t()) :: String.t() | nil + defp key_id_from_conn(conn) do + case HTTPSignatures.signature_for_conn(conn) do + %{"keyId" => key_id} -> + Signature.key_id_to_actor_url(key_id) + + _ -> + nil + end + end + + @spec actor_from_key_id(Plug.Conn.t()) :: Actor.t() | nil + defp actor_from_key_id(conn) do + with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn), + {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(key_actor_id) do + actor + else + _ -> + nil + end + end + + def call(%{assigns: %{actor: _}} = conn, _opts), do: conn + + # if this has payload make sure it is signed by the same actor that made it + def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do + with actor_id <- Utils.get_url(actor), + {:actor, %Actor{} = actor} <- {:actor, actor_from_key_id(conn)}, + {:actor_match, true} <- {:actor_match, actor.url == actor_id} do + assign(conn, :actor, actor) + else + {:actor_match, false} -> + Logger.debug("Failed to map identity from signature (payload actor mismatch)") + Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}") + assign(conn, :valid_signature, false) + + # remove me once testsuite uses mapped capabilities instead of what we do now + {:actor, nil} -> + Logger.debug("Failed to map identity from signature (lookup failure)") + Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}") + conn + end + end + + # no payload, probably a signed fetch + def call(%{assigns: %{valid_signature: true}} = conn, _opts) do + case actor_from_key_id(conn) do + %Actor{} = actor -> + assign(conn, :actor, actor) + + _ -> + Logger.debug("Failed to map identity from signature (no payload actor mismatch)") + Logger.debug("key_id=#{key_id_from_conn(conn)}") + assign(conn, :valid_signature, false) + end + end + + # no signature at all + def call(conn, _opts), do: conn +end diff --git a/lib/mobilizon_web/resolvers/admin.ex b/lib/mobilizon_web/resolvers/admin.ex index 21d4ec6c1..2e46bf46b 100644 --- a/lib/mobilizon_web/resolvers/admin.ex +++ b/lib/mobilizon_web/resolvers/admin.ex @@ -6,11 +6,15 @@ defmodule MobilizonWeb.Resolvers.Admin do import Mobilizon.Users.Guards alias Mobilizon.Admin.ActionLog + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor alias Mobilizon.Events alias Mobilizon.Events.{Event, Comment} alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Service.Statistics alias Mobilizon.Users.User + alias Mobilizon.Storage.Page + alias Mobilizon.Service.ActivityPub.Relay def list_action_logs( _parent, @@ -136,4 +140,76 @@ defmodule MobilizonWeb.Resolvers.Admin do def get_dashboard(_parent, _args, _resolution) do {:error, "You need to be logged-in and an administrator to access dashboard statistics"} end + + def list_relay_followers(_parent, %{page: page, limit: limit}, %{ + context: %{current_user: %User{role: role}} + }) + when is_admin(role) do + with %Actor{} = relay_actor <- Relay.get_actor() do + %Page{} = + page = Actors.list_external_followers_for_actor_paginated(relay_actor, page, limit) + + {:ok, page} + end + end + + def list_relay_followings(_parent, %{page: page, limit: limit}, %{ + context: %{current_user: %User{role: role}} + }) + when is_admin(role) do + with %Actor{} = relay_actor <- Relay.get_actor() do + %Page{} = + page = Actors.list_external_followings_for_actor_paginated(relay_actor, page, limit) + + {:ok, page} + end + end + + def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}}) + when is_admin(role) do + case Relay.follow(address) do + {:ok, _activity, follow} -> + {:ok, follow} + + {:error, {:error, err}} when is_bitstring(err) -> + {:error, err} + end + end + + def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}}) + when is_admin(role) do + case Relay.unfollow(address) do + {:ok, _activity, follow} -> + {:ok, follow} + + {:error, {:error, err}} when is_bitstring(err) -> + {:error, err} + end + end + + def accept_subscription(_parent, %{address: address}, %{ + context: %{current_user: %User{role: role}} + }) + when is_admin(role) do + case Relay.accept(address) do + {:ok, _activity, follow} -> + {:ok, follow} + + {:error, {:error, err}} when is_bitstring(err) -> + {:error, err} + end + end + + def reject_subscription(_parent, %{address: address}, %{ + context: %{current_user: %User{role: role}} + }) + when is_admin(role) do + case Relay.reject(address) do + {:ok, _activity, follow} -> + {:ok, follow} + + {:error, {:error, err}} when is_bitstring(err) -> + {:error, err} + end + end end diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 2efc53830..8d20b53ee 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -245,6 +245,9 @@ defmodule MobilizonWeb.Resolvers.Event do {:error, "Participant #{id} can't be approved since it's already a participant (with role #{role})"} + {:has_participation, nil} -> + {:error, "Participant not found"} + {:actor_approve_permission, _} -> {:error, "Provided moderator actor ID doesn't have permission on this event"} diff --git a/lib/mobilizon_web/resolvers/group.ex b/lib/mobilizon_web/resolvers/group.ex index c367a844e..92ea3a603 100644 --- a/lib/mobilizon_web/resolvers/group.ex +++ b/lib/mobilizon_web/resolvers/group.ex @@ -47,8 +47,8 @@ defmodule MobilizonWeb.Resolvers.Group do %{context: %{current_user: user}} ) do with creator_actor_id <- Map.get(args, :creator_actor_id), - {:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id), - args <- Map.put(args, :creator_actor, actor), + {:is_owned, %Actor{} = creator_actor} <- User.owns_actor(user, creator_actor_id), + args <- Map.put(args, :creator_actor, creator_actor), {:ok, _activity, %Actor{type: :Group} = group} <- API.Groups.create_group(args) do {:ok, group} diff --git a/lib/mobilizon_web/resolvers/person.ex b/lib/mobilizon_web/resolvers/person.ex index fb97f41a4..9084ae306 100644 --- a/lib/mobilizon_web/resolvers/person.ex +++ b/lib/mobilizon_web/resolvers/person.ex @@ -97,7 +97,7 @@ defmodule MobilizonWeb.Resolvers.Person do {:find_actor, Actors.get_actor(id)}, {:is_owned, %Actor{}} <- User.owns_actor(user, actor.id), args <- save_attached_pictures(args), - {:ok, actor} <- Actors.update_actor(actor, args) do + {:ok, _activity, %Actor{} = actor} <- ActivityPub.update(:actor, actor, args, true) do {:ok, actor} else {:find_actor, nil} -> diff --git a/lib/mobilizon_web/router.ex b/lib/mobilizon_web/router.ex index caf0c99e0..ba42d6588 100644 --- a/lib/mobilizon_web/router.ex +++ b/lib/mobilizon_web/router.ex @@ -16,6 +16,7 @@ defmodule MobilizonWeb.Router do pipeline :activity_pub_signature do plug(:accepts, ["activity-json", "html"]) plug(MobilizonWeb.HTTPSignaturePlug) + plug(MobilizonWeb.Plugs.MappedSignatureToIdentity) end pipeline :relay do @@ -91,6 +92,8 @@ defmodule MobilizonWeb.Router do scope "/", MobilizonWeb do pipe_through(:activity_pub_and_html) + pipe_through(:activity_pub_signature) + get("/@:name", PageController, :actor) get("/events/:uuid", PageController, :event) get("/comments/:uuid", PageController, :comment) diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex index e7f61efb3..c78bd3f75 100644 --- a/lib/mobilizon_web/schema.ex +++ b/lib/mobilizon_web/schema.ex @@ -20,6 +20,7 @@ defmodule MobilizonWeb.Schema do import_types(MobilizonWeb.Schema.ActorInterface) import_types(MobilizonWeb.Schema.Actors.PersonType) import_types(MobilizonWeb.Schema.Actors.GroupType) + import_types(MobilizonWeb.Schema.Actors.ApplicationType) import_types(MobilizonWeb.Schema.CommentType) import_types(MobilizonWeb.Schema.SearchType) import_types(MobilizonWeb.Schema.ConfigType) @@ -140,5 +141,13 @@ defmodule MobilizonWeb.Schema do import_fields(:feed_token_mutations) import_fields(:picture_mutations) import_fields(:report_mutations) + import_fields(:admin_mutations) + end + + @desc """ + Root subscription + """ + subscription do + import_fields(:person_subscriptions) end end diff --git a/lib/mobilizon_web/schema/actor.ex b/lib/mobilizon_web/schema/actor.ex index 89ea0cee5..2b21f1a2e 100644 --- a/lib/mobilizon_web/schema/actor.ex +++ b/lib/mobilizon_web/schema/actor.ex @@ -3,13 +3,10 @@ defmodule MobilizonWeb.Schema.ActorInterface do Schema representation for Actor """ use Absinthe.Schema.Notation - import Absinthe.Resolution.Helpers, only: [dataloader: 1] alias Mobilizon.Actors.Actor - alias Mobilizon.{Events} import_types(MobilizonWeb.Schema.Actors.FollowerType) import_types(MobilizonWeb.Schema.EventType) - # import_types(MobilizonWeb.Schema.PictureType) @desc "An ActivityPub actor" interface :actor do @@ -21,7 +18,6 @@ defmodule MobilizonWeb.Schema.ActorInterface do field(:local, :boolean, description: "If the actor is from this instance") field(:summary, :string, description: "The actor's summary") field(:preferred_username, :string, description: "The actor's preferred username") - field(:keys, :string, description: "The actors RSA Keys") field(:manually_approves_followers, :boolean, description: "Whether the actors manually approves followers" @@ -38,17 +34,6 @@ defmodule MobilizonWeb.Schema.ActorInterface do field(:followersCount, :integer, description: "Number of followers for this actor") field(:followingCount, :integer, description: "Number of actors following this actor") - # This one should have a privacy setting - field(:organized_events, list_of(:event), - resolve: dataloader(Events), - description: "A list of the events this actor has organized" - ) - - # This one is for the person itself **only** - # field(:feed, list_of(:event), description: "List of events the actor sees in his or her feed") - - # field(:memberships, list_of(:member)) - resolve_type(fn %Actor{type: :Person}, _ -> :person @@ -56,6 +41,9 @@ defmodule MobilizonWeb.Schema.ActorInterface do %Actor{type: :Group}, _ -> :group + %Actor{type: :Application}, _ -> + :application + _, _ -> nil end) diff --git a/lib/mobilizon_web/schema/actors/application.ex b/lib/mobilizon_web/schema/actors/application.ex new file mode 100644 index 000000000..8bfdf41fa --- /dev/null +++ b/lib/mobilizon_web/schema/actors/application.ex @@ -0,0 +1,38 @@ +defmodule MobilizonWeb.Schema.Actors.ApplicationType do + @moduledoc """ + Schema representation for Group. + """ + + use Absinthe.Schema.Notation + + @desc """ + Represents an application + """ + object :application do + interfaces([:actor]) + + field(:id, :id, description: "Internal ID for this application") + field(:url, :string, description: "The ActivityPub actor's URL") + field(:type, :actor_type, description: "The type of Actor (Person, Group,…)") + field(:name, :string, description: "The actor's displayed name") + field(:domain, :string, description: "The actor's domain if (null if it's this instance)") + field(:local, :boolean, description: "If the actor is from this instance") + field(:summary, :string, description: "The actor's summary") + field(:preferred_username, :string, description: "The actor's preferred username") + + field(:manually_approves_followers, :boolean, + description: "Whether the actors manually approves followers" + ) + + field(:suspended, :boolean, description: "If the actor is suspended") + + field(:avatar, :picture, description: "The actor's avatar picture") + field(:banner, :picture, description: "The actor's banner picture") + + # These one should have a privacy setting + field(:following, list_of(:follower), description: "List of followings") + field(:followers, list_of(:follower), description: "List of followers") + field(:followersCount, :integer, description: "Number of followers for this actor") + field(:followingCount, :integer, description: "Number of actors following this actor") + end +end diff --git a/lib/mobilizon_web/schema/actors/follower.ex b/lib/mobilizon_web/schema/actors/follower.ex index 50f3cb3ae..258103c1a 100644 --- a/lib/mobilizon_web/schema/actors/follower.ex +++ b/lib/mobilizon_web/schema/actors/follower.ex @@ -14,5 +14,13 @@ defmodule MobilizonWeb.Schema.Actors.FollowerType do field(:approved, :boolean, description: "Whether the follow has been approved by the target actor" ) + + field(:inserted_at, :datetime, description: "When the follow was created") + field(:updated_at, :datetime, description: "When the follow was updated") + end + + object :paginated_follower_list do + field(:elements, list_of(:follower), description: "A list of followers") + field(:total, :integer, description: "The total number of elements in the list") end end diff --git a/lib/mobilizon_web/schema/actors/group.ex b/lib/mobilizon_web/schema/actors/group.ex index 382df7f1c..a243ef6b3 100644 --- a/lib/mobilizon_web/schema/actors/group.ex +++ b/lib/mobilizon_web/schema/actors/group.ex @@ -27,7 +27,6 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do field(:local, :boolean, description: "If the actor is from this instance") field(:summary, :string, description: "The actor's summary") field(:preferred_username, :string, description: "The actor's preferred username") - field(:keys, :string, description: "The actors RSA Keys") field(:manually_approves_followers, :boolean, description: "Whether the actors manually approves followers" diff --git a/lib/mobilizon_web/schema/actors/person.ex b/lib/mobilizon_web/schema/actors/person.ex index bcddea046..2cbc4c78c 100644 --- a/lib/mobilizon_web/schema/actors/person.ex +++ b/lib/mobilizon_web/schema/actors/person.ex @@ -27,7 +27,6 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do field(:local, :boolean, description: "If the actor is from this instance") field(:summary, :string, description: "The actor's summary") field(:preferred_username, :string, description: "The actor's preferred username") - field(:keys, :string, description: "The actors RSA Keys") field(:manually_approves_followers, :boolean, description: "Whether the actors manually approves followers" @@ -160,4 +159,14 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do resolve(handle_errors(&Person.register_person/3)) end end + + object :person_subscriptions do + field :event_person_participation_changed, :person do + arg(:person_id, non_null(:id)) + + config(fn args, _ -> + {:ok, topic: args.person_id} + end) + end + end end diff --git a/lib/mobilizon_web/schema/admin.ex b/lib/mobilizon_web/schema/admin.ex index ca5ec8647..f1e7543f5 100644 --- a/lib/mobilizon_web/schema/admin.ex +++ b/lib/mobilizon_web/schema/admin.ex @@ -71,5 +71,49 @@ defmodule MobilizonWeb.Schema.AdminType do field :dashboard, type: :dashboard do resolve(&Admin.get_dashboard/3) end + + field :relay_followers, type: :paginated_follower_list do + arg(:page, :integer, default_value: 1) + arg(:limit, :integer, default_value: 10) + resolve(&Admin.list_relay_followers/3) + end + + field :relay_followings, type: :paginated_follower_list do + arg(:page, :integer, default_value: 1) + arg(:limit, :integer, default_value: 10) + arg(:order_by, :string, default_value: :updated_at) + arg(:direction, :string, default_value: :desc) + resolve(&Admin.list_relay_followings/3) + end + end + + object :admin_mutations do + @desc "Add a relay subscription" + field :add_relay, type: :follower do + arg(:address, non_null(:string)) + + resolve(&Admin.create_relay/3) + end + + @desc "Delete a relay subscription" + field :remove_relay, type: :follower do + arg(:address, non_null(:string)) + + resolve(&Admin.remove_relay/3) + end + + @desc "Accept a relay subscription" + field :accept_relay, type: :follower do + arg(:address, non_null(:string)) + + resolve(&Admin.accept_subscription/3) + end + + @desc "Reject a relay subscription" + field :reject_relay, type: :follower do + arg(:address, non_null(:string)) + + resolve(&Admin.reject_subscription/3) + end end end diff --git a/lib/mobilizon_web/schema/report.ex b/lib/mobilizon_web/schema/report.ex index 65b0344a9..e0edc07e6 100644 --- a/lib/mobilizon_web/schema/report.ex +++ b/lib/mobilizon_web/schema/report.ex @@ -75,6 +75,7 @@ defmodule MobilizonWeb.Schema.ReportType do arg(:reported_id, non_null(:id)) arg(:event_id, :id, default_value: nil) arg(:comments_ids, list_of(:id), default_value: []) + arg(:forward, :boolean, default_value: false) resolve(&Report.create_report/3) end diff --git a/lib/mobilizon_web/templates/email/report.html.eex b/lib/mobilizon_web/templates/email/report.html.eex index 6e15092f0..e4f26d063 100644 --- a/lib/mobilizon_web/templates/email/report.html.eex +++ b/lib/mobilizon_web/templates/email/report.html.eex @@ -35,7 +35,11 @@

- <%= gettext "%{reporter_name} (%{reporter_username}) reported the following content.", reporter_name: @report.reporter.name, reporter_username: Mobilizon.Actors.Actor.preferred_username_and_domain(@report.reporter) %> + <%= if @report.reporter.type == :Application and @report.reporter.preferred_username == "relay" do %> + <%= gettext "Someone on %{instance} reported the following content.", instance: @report.reporter.domain %> + <% else %> + <%= gettext "%{reporter_name} (%{reporter_username}) reported the following content.", reporter_name: @report.reporter.name, reporter_username: Mobilizon.Actors.Actor.preferred_username_and_domain(@report.reporter) %> + <% end %>

@@ -59,10 +63,10 @@ <%= if Map.has_key?(@report, :comments) && length(@report.comments) > 0 do %> -

<%= gettext "Comments" %>

+

<%= gettext "Comments" %>

<%= for comment <- @report.comments do %>

- <%= comment.text %> + <%= HtmlSanitizeEx.strip_tags(comment.text) %>

<% end %> diff --git a/lib/mobilizon_web/templates/email/report.text.eex b/lib/mobilizon_web/templates/email/report.text.eex index dae6e72fd..d054fa429 100644 --- a/lib/mobilizon_web/templates/email/report.text.eex +++ b/lib/mobilizon_web/templates/email/report.text.eex @@ -4,20 +4,26 @@ <%= if Map.has_key?(@report, :event) do %> <%= gettext "Event" %> + <%= @report.event.title %> <% end %> + <%= if Map.has_key?(@report, :comments) && length(@report.comments) > 0 do %> <%= gettext "Comments" %> + <%= for comment <- @report.comments do %> <%= comment.text %> <% end %> <% end %> + <%= if @report.content do %> <%= gettext "Reason" %> + <%= @report.content %> <% end %> + View the report: <%= moderation_report_url(MobilizonWeb.Endpoint, :index, @report.id) %> diff --git a/lib/mobilizon_web/upload.ex b/lib/mobilizon_web/upload.ex index 3ba692af1..5920ce586 100644 --- a/lib/mobilizon_web/upload.ex +++ b/lib/mobilizon_web/upload.ex @@ -148,6 +148,21 @@ defmodule MobilizonWeb.Upload do end end + defp prepare_upload(%{body: body, name: name} = _file, opts) do + with :ok <- check_binary_size(body, opts.size_limit), + tmp_path <- tempfile_for_image(body), + {:ok, content_type, name} <- MIME.file_mime_type(tmp_path, name) do + {:ok, + %__MODULE__{ + id: UUID.generate(), + name: name, + tempfile: tmp_path, + content_type: content_type, + size: byte_size(body) + }} + end + end + defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do with {:ok, %{size: size}} <- File.stat(path), true <- size <= size_limit do @@ -160,6 +175,23 @@ defmodule MobilizonWeb.Upload do defp check_file_size(_, _), do: :ok + defp check_binary_size(binary, size_limit) + when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do + {:error, :file_too_large} + end + + defp check_binary_size(_, _), do: :ok + + # Creates a tempfile using the Plug.Upload Genserver which cleans them up + # automatically. + defp tempfile_for_image(data) do + {:ok, tmp_path} = Plug.Upload.random_file("temp_files") + {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary]) + IO.binwrite(tmp_file, data) + + tmp_path + end + defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do path = URI.encode(path, &char_unescaped?/1) <> diff --git a/lib/mobilizon_web/views/activity_pub/actor_view.ex b/lib/mobilizon_web/views/activity_pub/actor_view.ex index e384b95d6..284e12271 100644 --- a/lib/mobilizon_web/views/activity_pub/actor_view.ex +++ b/lib/mobilizon_web/views/activity_pub/actor_view.ex @@ -4,44 +4,13 @@ defmodule MobilizonWeb.ActivityPub.ActorView do alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Service.ActivityPub - alias Mobilizon.Service.ActivityPub.{Activity, Utils} + alias Mobilizon.Service.ActivityPub.{Activity, Utils, Convertible} @private_visibility_empty_collection %{elements: [], total: 0} def render("actor.json", %{actor: actor}) do - public_key = Utils.pem_to_public_key_pem(actor.keys) - - %{ - "id" => actor.url, - "type" => to_string(actor.type), - "following" => actor.following_url, - "followers" => actor.followers_url, - "inbox" => actor.inbox_url, - "outbox" => actor.outbox_url, - "preferredUsername" => actor.preferred_username, - "name" => actor.name, - "summary" => actor.summary, - "url" => actor.url, - "manuallyApprovesFollowers" => actor.manually_approves_followers, - "publicKey" => %{ - "id" => "#{actor.url}#main-key", - "owner" => actor.url, - "publicKeyPem" => public_key - }, - # TODO : Make have actors have an uuid - # "uuid" => actor.uuid - "endpoints" => %{ - "sharedInbox" => actor.shared_inbox_url - } - # "icon" => %{ - # "type" => "Image", - # "url" => User.avatar_url(actor) - # }, - # "image" => %{ - # "type" => "Image", - # "url" => User.banner_url(actor) - # } - } + actor + |> Convertible.model_to_as() |> Map.merge(Utils.make_json_ld_header()) end diff --git a/lib/mobilizon_web/views/page_view.ex b/lib/mobilizon_web/views/page_view.ex index e3eea5c2c..33b01cc74 100644 --- a/lib/mobilizon_web/views/page_view.ex +++ b/lib/mobilizon_web/views/page_view.ex @@ -4,69 +4,34 @@ defmodule MobilizonWeb.PageView do """ use MobilizonWeb, :view alias Mobilizon.Actors.Actor - alias Mobilizon.Service.ActivityPub.{Converter, Utils} + alias Mobilizon.Tombstone + alias Mobilizon.Service.ActivityPub.{Convertible, Utils} alias Mobilizon.Service.Metadata alias Mobilizon.Service.MetadataUtils alias Mobilizon.Service.Metadata.Instance + alias Mobilizon.Events.{Comment, Event} - def render("actor.activity-json", %{conn: %{assigns: %{object: actor}}}) do - public_key = Utils.pem_to_public_key_pem(actor.keys) - - %{ - "id" => Actor.build_url(actor.preferred_username, :page), - "type" => "Person", - "following" => Actor.build_url(actor.preferred_username, :following), - "followers" => Actor.build_url(actor.preferred_username, :followers), - "inbox" => Actor.build_url(actor.preferred_username, :inbox), - "outbox" => Actor.build_url(actor.preferred_username, :outbox), - "preferredUsername" => actor.preferred_username, - "name" => actor.name, - "summary" => actor.summary, - "url" => actor.url, - "manuallyApprovesFollowers" => actor.manually_approves_followers, - "publicKey" => %{ - "id" => "#{actor.url}#main-key", - "owner" => actor.url, - "publicKeyPem" => public_key - }, - # TODO : Make have actors have an uuid - # "uuid" => actor.uuid - "endpoints" => %{ - "sharedInbox" => actor.shared_inbox_url - } - # "icon" => %{ - # "type" => "Image", - # "url" => User.avatar_url(actor) - # }, - # "image" => %{ - # "type" => "Image", - # "url" => User.banner_url(actor) - # } - } + def render("actor.activity-json", %{conn: %{assigns: %{object: %Actor{} = actor}}}) do + actor + |> Convertible.model_to_as() |> Map.merge(Utils.make_json_ld_header()) end - def render("event.activity-json", %{conn: %{assigns: %{object: event}}}) do + def render("event.activity-json", %{conn: %{assigns: %{object: %Event{} = event}}}) do event - |> Converter.Event.model_to_as() + |> Convertible.model_to_as() |> Map.merge(Utils.make_json_ld_header()) end - def render("comment.activity-json", %{conn: %{assigns: %{object: comment}}}) do - comment = Converter.Comment.model_to_as(comment) + def render("event.activity-json", %{conn: %{assigns: %{object: %Tombstone{} = event}}}) do + event + |> Convertible.model_to_as() + |> Map.merge(Utils.make_json_ld_header()) + end - %{ - "actor" => comment["actor"], - "uuid" => comment["uuid"], - # The activity should have attributedTo, not the comment itself - # "attributedTo" => comment.attributed_to, - "type" => "Note", - "id" => comment["id"], - "content" => comment["content"], - "mediaType" => "text/html" - # "published" => Timex.format!(comment.inserted_at, "{ISO:Extended}"), - # "updated" => Timex.format!(comment.updated_at, "{ISO:Extended}") - } + def render("comment.activity-json", %{conn: %{assigns: %{object: %Comment{} = comment}}}) do + comment + |> Convertible.model_to_as() |> Map.merge(Utils.make_json_ld_header()) end @@ -74,7 +39,9 @@ defmodule MobilizonWeb.PageView do when page in ["actor.html", "event.html", "comment.html"] do with {:ok, index_content} <- File.read(index_file_path()) do tags = object |> Metadata.build_tags() |> MetadataUtils.stringify_tags() - index_content = String.replace(index_content, "", tags) + + index_content = replace_meta(index_content, tags) + {:safe, index_content} end end @@ -82,7 +49,9 @@ defmodule MobilizonWeb.PageView do def render("index.html", _assigns) do with {:ok, index_content} <- File.read(index_file_path()) do tags = Instance.build_tags() |> MetadataUtils.stringify_tags() - index_content = String.replace(index_content, "", tags) + + index_content = replace_meta(index_content, tags) + {:safe, index_content} end end @@ -90,4 +59,11 @@ defmodule MobilizonWeb.PageView do defp index_file_path do Path.join(Application.app_dir(:mobilizon, "priv/static"), "index.html") end + + # TODO: Find why it's different in dev/prod and during tests + defp replace_meta(index_content, tags) do + index_content + |> String.replace("", tags) + |> String.replace("", tags) + end end diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 8e3d4a0ee..4c612c803 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -11,7 +11,7 @@ defmodule Mobilizon.Service.ActivityPub do import Mobilizon.Service.ActivityPub.Utils import Mobilizon.Service.ActivityPub.Visibility - alias Mobilizon.{Actors, Config, Events, Reports, Users} + alias Mobilizon.{Actors, Config, Events, Reports, Users, Share} alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Events.{Comment, Event, Participant} alias Mobilizon.Reports.Report @@ -50,6 +50,15 @@ defmodule Mobilizon.Service.ActivityPub do def fetch_object_from_url(url) do Logger.info("Fetching object from url #{url}") + date = Mobilizon.Service.HTTPSignatures.Signature.generate_date_header() + + headers = + [{:Accept, "application/activity+json"}] + |> maybe_date_fetch(date) + |> sign_fetch(url, date) + + Logger.debug("Fetch headers: #{inspect(headers)}") + with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")}, {:existing_event, nil} <- {:existing_event, Events.get_event_by_url(url)}, {:existing_comment, nil} <- {:existing_comment, Events.get_comment_from_url(url)}, @@ -58,12 +67,13 @@ defmodule Mobilizon.Service.ActivityPub do {:ok, %{body: body, status_code: code}} when code in 200..299 <- HTTPoison.get( url, - [Accept: "application/activity+json"], + headers, follow_redirect: true, timeout: 10_000, recv_timeout: 20_000 ), {:ok, data} <- Jason.decode(body), + {:origin_check, true} <- {:origin_check, origin_check?(url, data)}, params <- %{ "type" => "Create", "to" => data["to"], @@ -95,6 +105,10 @@ defmodule Mobilizon.Service.ActivityPub do {:existing_actor, {:ok, %Actor{url: actor_url}}} -> {:ok, Actors.get_actor_by_url!(actor_url, true)} + {:origin_check, false} -> + Logger.warn("Object origin check failed") + {:error, "Object origin check failed"} + e -> {:error, e} end @@ -114,9 +128,9 @@ defmodule Mobilizon.Service.ActivityPub do {:ok, %Actor{} = actor} -> {:ok, actor} - _ -> + err -> Logger.warn("Could not fetch by AP id") - + Logger.debug(inspect(err)) {:error, "Could not fetch by AP id"} end end @@ -184,11 +198,13 @@ defmodule Mobilizon.Service.ActivityPub do end end - def accept(type, entity, args, local \\ false, additional \\ %{}) do + def accept(type, entity, local \\ true, additional \\ %{}) do + Logger.debug("We're accepting something") + {:ok, entity, update_data} = case type do - :join -> accept_join(entity, args, additional) - :follow -> accept_follow(entity, args, additional) + :join -> accept_join(entity, additional) + :follow -> accept_follow(entity, additional) end with {:ok, activity} <- create_activity(update_data, local), @@ -202,63 +218,24 @@ defmodule Mobilizon.Service.ActivityPub do end end - def reject(%{to: to, actor: actor, object: object} = params, activity_wrapper_id \\ nil) do - # only accept false as false value - local = !(params[:local] == false) + def reject(type, entity, local \\ true, additional \\ %{}) do + {:ok, entity, update_data} = + case type do + :join -> reject_join(entity, additional) + :follow -> reject_follow(entity, additional) + end - with data <- %{ - "to" => to, - "type" => "Reject", - "actor" => actor, - "object" => object, - "id" => activity_wrapper_id || get_url(object) <> "/activity" - }, - {:ok, activity} <- create_activity(data, local), - {:ok, object} <- insert_full_object(data), + with {:ok, activity} <- create_activity(update_data, local), :ok <- maybe_federate(activity) do - {:ok, activity, object} + {:ok, activity, entity} + else + err -> + Logger.error("Something went wrong while creating an activity") + Logger.debug(inspect(err)) + err end end - # TODO: This is weird, maybe we shouldn't check here if we can make the activity. - # def like( - # %Actor{url: url} = actor, - # object, - # activity_id \\ nil, - # local \\ true - # ) do - # with nil <- get_existing_like(url, object), - # like_data <- make_like_data(user, object, activity_id), - # {:ok, activity} <- create_activity(like_data, local), - # {:ok, object} <- insert_full_object(data), - # {:ok, object} <- add_like_to_object(activity, object), - # :ok <- maybe_federate(activity) do - # {:ok, activity, object} - # else - # %Activity{} = activity -> {:ok, activity, object} - # error -> {:error, error} - # end - # end - - # def unlike( - # %User{} = actor, - # %Object{} = object, - # activity_id \\ nil, - # local \\ true - # ) do - # with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), - # unlike_data <- make_unlike_data(actor, like_activity, activity_id), - # {:ok, unlike_activity} <- create_activity(unlike_data, local), - # {:ok, _object} <- insert_full_object(data), - # {:ok, _activity} <- Repo.delete(like_activity), - # {:ok, object} <- remove_like_from_object(like_activity, object), - # :ok <- maybe_federate(unlike_activity) do - # {:ok, unlike_activity, like_activity, object} - # else - # _e -> {:ok, object} - # end - # end - def announce( %Actor{} = actor, object, @@ -267,9 +244,10 @@ defmodule Mobilizon.Service.ActivityPub do public \\ true ) do with true <- is_public?(object), + {:ok, %Actor{id: object_owner_actor_id}} <- Actors.get_actor_by_url(object["actor"]), + {:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id), announce_data <- make_announce_data(actor, object, activity_id, public), {:ok, activity} <- create_activity(announce_data, local), - {:ok, object} <- insert_full_object(announce_data), :ok <- maybe_federate(activity) do {:ok, activity, object} else @@ -288,7 +266,6 @@ defmodule Mobilizon.Service.ActivityPub do with announce_activity <- make_announce_data(actor, object, cancelled_activity_id), unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id), {:ok, unannounce_activity} <- create_activity(unannounce_data, local), - {:ok, object} <- insert_full_object(unannounce_data), :ok <- maybe_federate(unannounce_activity) do {:ok, unannounce_activity, object} else @@ -327,9 +304,8 @@ defmodule Mobilizon.Service.ActivityPub do unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id), {:ok, activity} <- create_activity(unfollow_data, local), - {:ok, object} <- insert_full_object(unfollow_data), :ok <- maybe_federate(activity) do - {:ok, activity, object} + {:ok, activity, follow} else err -> Logger.debug("Error while unfollowing an actor #{inspect(err)}") @@ -339,6 +315,7 @@ defmodule Mobilizon.Service.ActivityPub do 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", @@ -348,15 +325,19 @@ defmodule Mobilizon.Service.ActivityPub do "id" => url <> "/delete" } - with {:ok, %Event{} = event} <- Events.delete_event(event), + with audience <- + Audience.calculate_to_and_cc_from_mentions(event), + {:ok, %Event{} = event} <- Events.delete_event(event), {:ok, %Tombstone{} = _tombstone} <- Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}), - {:ok, activity} <- create_activity(data, local), + Share.delete_all_by_uri(event.url), + {:ok, activity} <- create_activity(Map.merge(data, audience), local), :ok <- maybe_federate(activity) do {:ok, activity, event} 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", @@ -366,10 +347,13 @@ defmodule Mobilizon.Service.ActivityPub do "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] } - with {:ok, %Comment{} = comment} <- Events.delete_comment(comment), + with audience <- + Audience.calculate_to_and_cc_from_mentions(comment), + {:ok, %Comment{} = comment} <- Events.delete_comment(comment), {:ok, %Tombstone{} = _tombstone} <- Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}), - {:ok, activity} <- create_activity(data, local), + Share.delete_all_by_uri(comment.url), + {:ok, activity} <- create_activity(Map.merge(data, audience), local), :ok <- maybe_federate(activity) do {:ok, activity, comment} end @@ -384,7 +368,7 @@ defmodule Mobilizon.Service.ActivityPub do "to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] } - with {:ok, %Actor{} = actor} <- Actors.delete_actor(actor), + with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor), {:ok, activity} <- create_activity(data, local), :ok <- maybe_federate(activity) do {:ok, activity, actor} @@ -396,6 +380,8 @@ defmodule Mobilizon.Service.ActivityPub do {: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 -> @@ -413,52 +399,56 @@ defmodule Mobilizon.Service.ActivityPub do end end - def join(object, actor, local \\ true) + def join(object, actor, local \\ true, additional \\ %{}) - def join(%Event{options: options} = event, %Actor{} = actor, local) do + def join(%Event{} = event, %Actor{} = actor, local, additional) do # TODO Refactor me for federation - with maximum_attendee_capacity <- - Map.get(options, :maximum_attendee_capacity) || 0, - {:maximum_attendee_capacity, true} <- - {:maximum_attendee_capacity, - maximum_attendee_capacity == 0 || - Mobilizon.Events.count_participant_participants(event.id) < - maximum_attendee_capacity}, - role <- Mobilizon.Events.get_default_participant_role(event), + with {:maximum_attendee_capacity, true} <- + {:maximum_attendee_capacity, check_attendee_capacity(event)}, {:ok, %Participant{} = participant} <- Mobilizon.Events.create_participant(%{ - role: role, + role: :not_approved, event_id: event.id, - actor_id: actor.id + actor_id: actor.id, + url: Map.get(additional, :url) }), join_data <- Convertible.model_to_as(participant), - join_data <- Map.put(join_data, "to", [event.organizer_actor.url]), - join_data <- Map.put(join_data, "cc", []), - {:ok, activity} <- create_activity(join_data, local), - {:ok, _object} <- insert_full_object(join_data), + audience <- + Audience.calculate_to_and_cc_from_mentions(participant), + {:ok, activity} <- create_activity(Map.merge(join_data, audience), local), :ok <- maybe_federate(activity) do - if role === :participant do - accept_join( + if event.local && Mobilizon.Events.get_default_participant_role(event) === :participant do + accept( + :join, participant, - %{} + true, + %{"actor" => event.organizer_actor.url} ) + else + {:ok, activity, participant} end - - {:ok, activity, participant} end end # TODO: Implement me - def join(%Actor{type: :Group} = _group, %Actor{} = _actor, _local) do + 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 + end + end + def leave(object, actor, local \\ true) # TODO: If we want to use this for exclusion we need to have an extra field # for the actor that excluded the participant def leave( - %Event{id: event_id, url: event_url} = event, + %Event{id: event_id, url: event_url} = _event, %Actor{id: actor_id, url: actor_url} = _actor, local ) do @@ -473,11 +463,11 @@ defmodule Mobilizon.Service.ActivityPub do # If it's an exclusion it should be something else "actor" => actor_url, "object" => event_url, - "to" => [event.organizer_actor.url], - "cc" => [] + "id" => "#{MobilizonWeb.Endpoint.url()}/leave/event/#{participant.id}" }, - {:ok, activity} <- create_activity(leave_data, local), - {:ok, _object} <- insert_full_object(leave_data), + audience <- + Audience.calculate_to_and_cc_from_mentions(participant), + {:ok, activity} <- create_activity(Map.merge(leave_data, audience), local), :ok <- maybe_federate(activity) do {:ok, activity, participant} end @@ -537,16 +527,22 @@ defmodule Mobilizon.Service.ActivityPub do end end + @spec is_create_activity?(Activity.t()) :: boolean + defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true + defp is_create_activity?(_), do: false + @doc """ Publish an activity to all appropriated audiences inboxes """ + @spec publish(Actor.t(), Activity.t()) :: :ok def publish(actor, activity) do Logger.debug("Publishing an activity") Logger.debug(inspect(activity)) public = is_public?(activity) + Logger.debug("is public ? #{public}") - if public && !is_delete_activity?(activity) && Config.get([:instance, :allow_relay]) do + if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) Relay.publish(activity) @@ -578,15 +574,12 @@ defmodule Mobilizon.Service.ActivityPub do end) end - defp is_delete_activity?(%Activity{data: %{"type" => "Delete"}}), do: true - defp is_delete_activity?(_), do: false - @doc """ Publish an activity to a specific inbox """ def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do Logger.info("Federating #{id} to #{inbox}") - %URI{host: host, path: _path} = URI.parse(inbox) + %URI{host: host, path: path} = URI.parse(inbox) digest = Signature.build_digest(json) date = Signature.generate_date_header() @@ -594,10 +587,9 @@ defmodule Mobilizon.Service.ActivityPub do signature = Signature.sign(actor, %{ + "(request-target)": "post #{path}", host: host, "content-length": byte_size(json), - # TODO : Look me up in depth why Pleroma handles this inside lib/mobilizon_web/http_signature.ex - # "(request-target)": request_target, digest: digest, date: date }) @@ -627,7 +619,7 @@ defmodule Mobilizon.Service.ActivityPub do :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") - actor_data_from_actor_object(data) + Mobilizon.Service.ActivityPub.Converter.Actor.as_to_model_data(data) else # Actor is gone, probably deleted {:ok, %HTTPoison.Response{status_code: 410}} -> @@ -642,49 +634,6 @@ defmodule Mobilizon.Service.ActivityPub do res end - @doc """ - Creating proper actor data struct from AP data - - - Convert ActivityPub data to our internal format - """ - @spec actor_data_from_actor_object(map()) :: {:ok, map()} - def actor_data_from_actor_object(data) when is_map(data) do - avatar = - data["icon"]["url"] && - %{ - "name" => data["icon"]["name"] || "avatar", - "url" => data["icon"]["url"] - } - - banner = - data["image"]["url"] && - %{ - "name" => data["image"]["name"] || "banner", - "url" => data["image"]["url"] - } - - actor_data = %{ - url: data["id"], - avatar: avatar, - banner: banner, - name: data["name"], - preferred_username: data["preferredUsername"], - summary: data["summary"], - keys: data["publicKey"]["publicKeyPem"], - inbox_url: data["inbox"], - outbox_url: data["outbox"], - following_url: data["following"], - followers_url: data["followers"], - shared_inbox_url: data["endpoints"]["sharedInbox"], - domain: URI.parse(data["id"]).host, - manually_approves_followers: data["manuallyApprovesFollowers"], - type: data["type"] - } - - {:ok, actor_data} - end - @doc """ Return all public activities (events & comments) for an actor """ @@ -736,12 +685,7 @@ defmodule Mobilizon.Service.ActivityPub do {:ok, %Event{} = event} <- Events.create_event(args), event_as_data <- Convertible.model_to_as(event), audience <- - Audience.calculate_to_and_cc_from_mentions( - event.organizer_actor, - args.mentions, - nil, - event.visibility - ), + 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} @@ -754,12 +698,7 @@ defmodule Mobilizon.Service.ActivityPub do {:ok, %Comment{} = comment} <- Events.create_comment(args), comment_as_data <- Convertible.model_to_as(comment), audience <- - Audience.calculate_to_and_cc_from_mentions( - comment.actor, - args.mentions, - args.in_reply_to_comment, - comment.visibility - ), + 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} @@ -771,13 +710,7 @@ defmodule Mobilizon.Service.ActivityPub 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 <- - Audience.calculate_to_and_cc_from_mentions( - args.creator_actor, - [], - nil, - :public - ), + 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} @@ -799,12 +732,7 @@ defmodule Mobilizon.Service.ActivityPub do {:ok, %Event{} = new_event} <- Events.update_event(old_event, args), event_as_data <- Convertible.model_to_as(new_event), audience <- - Audience.calculate_to_and_cc_from_mentions( - new_event.organizer_actor, - Map.get(args, :mentions, []), - nil, - new_event.visibility - ), + 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 @@ -821,34 +749,29 @@ defmodule Mobilizon.Service.ActivityPub do with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args), actor_as_data <- Convertible.model_to_as(new_actor), audience <- - Audience.calculate_to_and_cc_from_mentions( - new_actor, - [], - nil, - :public - ), + 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 accept_follow(Follower.t(), map(), map()) :: + @spec accept_follow(Follower.t(), map()) :: {:ok, Follower.t(), Activity.t()} | any() defp accept_follow( %Follower{} = follower, - args, additional ) do - with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, args), + with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}), follower_as_data <- Convertible.model_to_as(follower), - audience <- - Audience.calculate_to_and_cc_from_mentions(follower.target_actor), update_data <- - make_update_data( + make_accept_join_data( follower_as_data, - Map.merge(Map.merge(audience, additional), %{ - "id" => "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follower.id}" + Map.merge(additional, %{ + "id" => "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follower.id}", + "to" => [follower.actor.url], + "cc" => [], + "actor" => follower.target_actor.url }) ) do {:ok, follower, update_data} @@ -860,17 +783,20 @@ defmodule Mobilizon.Service.ActivityPub do end end - @spec accept_join(Participant.t(), map(), map()) :: + @spec accept_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any() defp accept_join( %Participant{} = participant, - args, - additional \\ %{} + additional ) do - with {:ok, %Participant{} = participant} <- Events.update_participant(participant, args), + with {:ok, %Participant{} = participant} <- + Events.update_participant(participant, %{role: :participant}), + Absinthe.Subscription.publish(MobilizonWeb.Endpoint, participant.actor, + event_person_participation_changed: participant.actor.id + ), participant_as_data <- Convertible.model_to_as(participant), audience <- - Audience.calculate_to_and_cc_from_mentions(participant.actor), + Audience.calculate_to_and_cc_from_mentions(participant), update_data <- make_accept_join_data( participant_as_data, @@ -887,6 +813,66 @@ defmodule Mobilizon.Service.ActivityPub do end end + @spec reject_join(Participant.t(), map()) :: + {:ok, Participant.t(), Activity.t()} | any() + defp reject_join(%Participant{} = participant, additional) do + with {:ok, %Participant{} = participant} <- + Events.update_participant(participant, %{approved: false, role: :rejected}), + Absinthe.Subscription.publish(MobilizonWeb.Endpoint, participant.actor, + event_person_participation_changed: participant.actor.id + ), + participant_as_data <- Convertible.model_to_as(participant), + audience <- + participant + |> Audience.calculate_to_and_cc_from_mentions() + |> Map.merge(additional), + reject_data <- %{ + "type" => "Reject", + "object" => participant_as_data + }, + update_data <- + reject_data + |> Map.merge(audience) + |> Map.merge(%{ + "id" => "#{MobilizonWeb.Endpoint.url()}/reject/join/#{participant.id}" + }) do + {:ok, participant, update_data} + else + err -> + Logger.error("Something went wrong while creating an update activity") + Logger.debug(inspect(err)) + err + end + end + + @spec reject_follow(Follower.t(), map()) :: + {:ok, Follower.t(), Activity.t()} | any() + defp reject_follow(%Follower{} = follower, additional) do + with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower), + follower_as_data <- Convertible.model_to_as(follower), + audience <- + follower.actor |> Audience.calculate_to_and_cc_from_mentions() |> Map.merge(additional), + reject_data <- %{ + "to" => follower.actor.url, + "type" => "Reject", + "actor" => follower.actor.url, + "object" => follower_as_data + }, + update_data <- + reject_data + |> Map.merge(audience) + |> Map.merge(%{ + "id" => "#{MobilizonWeb.Endpoint.url()}/reject/follow/#{follower.id}" + }) do + {:ok, follower, update_data} + else + err -> + Logger.error("Something went wrong while creating an update activity") + Logger.debug(inspect(err)) + 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 @@ -923,7 +909,8 @@ defmodule Mobilizon.Service.ActivityPub do # 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) |> Events.get_comment(), + args |> Map.get(:in_reply_to_comment_id) |> Events.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( @@ -940,6 +927,7 @@ defmodule Mobilizon.Service.ActivityPub do 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)), @@ -953,6 +941,16 @@ defmodule Mobilizon.Service.ActivityPub do 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) |> HtmlSanitizeEx.strip_tags() |> String.trim(), diff --git a/lib/service/activity_pub/audience.ex b/lib/service/activity_pub/audience.ex index 9f4c16cd3..cca7d1f66 100644 --- a/lib/service/activity_pub/audience.ex +++ b/lib/service/activity_pub/audience.ex @@ -2,7 +2,13 @@ defmodule Mobilizon.Service.ActivityPub.Audience do @moduledoc """ Tools for calculating content audience """ + alias Mobilizon.Actors alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Comment + alias Mobilizon.Events.Event + alias Mobilizon.Events.Participant + alias Mobilizon.Share + require Logger @ap_public "https://www.w3.org/ns/activitystreams#Public" @@ -13,35 +19,27 @@ defmodule Mobilizon.Service.ActivityPub.Audience do * `to` : the mentioned actors, the eventual actor we're replying to and the public * `cc` : the actor's followers """ - @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()} - def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :public) do + @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} + def get_to_and_cc(%Actor{} = actor, mentions, :public) do to = [@ap_public | mentions] cc = [actor.followers_url] - if in_reply_to do - {Enum.uniq([in_reply_to.actor | to]), cc} - else - {to, cc} - end + {to, cc} end @doc """ Determines the full audience based on mentions based on a unlisted audience Audience is: - * `to` : the mentionned actors, actor's followers and the eventual actor we're replying to + * `to` : the mentioned actors, actor's followers and the eventual actor we're replying to * `cc` : public """ - @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()} - def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :unlisted) do + @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} + def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do to = [actor.followers_url | mentions] cc = [@ap_public] - if in_reply_to do - {Enum.uniq([in_reply_to.actor | to]), cc} - else - {to, cc} - end + {to, cc} end @doc """ @@ -51,9 +49,9 @@ defmodule Mobilizon.Service.ActivityPub.Audience do * `to` : the mentioned actors, actor's followers and the eventual actor we're replying to * `cc` : none """ - @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()} - def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :private) do - {to, cc} = get_to_and_cc(actor, mentions, in_reply_to, :direct) + @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} + def get_to_and_cc(%Actor{} = actor, mentions, :private) do + {to, cc} = get_to_and_cc(actor, mentions, :direct) {[actor.followers_url | to], cc} end @@ -64,16 +62,12 @@ defmodule Mobilizon.Service.ActivityPub.Audience do * `to` : the mentioned actors and the eventual actor we're replying to * `cc` : none """ - @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()} - def get_to_and_cc(_actor, mentions, in_reply_to, :direct) do - if in_reply_to do - {Enum.uniq([in_reply_to.actor | mentions]), []} - else - {mentions, []} - end + @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} + def get_to_and_cc(_actor, mentions, :direct) do + {mentions, []} end - def get_to_and_cc(_actor, mentions, _in_reply_to, {:list, _}) do + def get_to_and_cc(_actor, mentions, {:list, _}) do {mentions, []} end @@ -83,16 +77,109 @@ defmodule Mobilizon.Service.ActivityPub.Audience do def get_addressed_actors(mentioned_users, _), do: mentioned_users - def calculate_to_and_cc_from_mentions( - actor, - mentions \\ [], - in_reply_to \\ nil, - visibility \\ :public - ) do - with mentioned_actors <- for({_, mentioned_actor} <- mentions, do: mentioned_actor.url), + 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), - {to, cc} <- get_to_and_cc(actor, addressed_actors, in_reply_to, visibility) do + {to, cc} <- get_to_and_cc(comment.actor, addressed_actors, comment.visibility), + {to, cc} <- {Enum.uniq(to ++ add_in_reply_to(comment.in_reply_to_comment)), cc}, + {to, cc} <- {Enum.uniq(to ++ add_event_author(comment.event)), cc}, + {to, cc} <- + {to, + Enum.uniq( + cc ++ + add_comments_authors([comment.origin_comment]) ++ + add_shares_actors_followers(comment.url) + )} do %{"to" => to, "cc" => 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), + {to, cc} <- get_to_and_cc(event.organizer_actor, addressed_actors, event.visibility), + {to, cc} <- + {to, + Enum.uniq( + cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url) + )} do + %{"to" => to, "cc" => cc} + end + end + + def calculate_to_and_cc_from_mentions(%Participant{} = participant) do + participant = Mobilizon.Storage.Repo.preload(participant, [:actor, :event]) + + actor_participants_urls = + participant.event.id + |> Mobilizon.Events.list_actors_participants_for_event() + |> Enum.map(& &1.url) + + %{"to" => [participant.actor.url], "cc" => actor_participants_urls} + end + + def calculate_to_and_cc_from_mentions(%Actor{} = actor) do + %{ + "to" => [@ap_public], + "cc" => [actor.followers_url] ++ add_actors_that_had_our_content(actor.id) + } + end + + defp add_in_reply_to(%Comment{actor: %Actor{url: url}} = _comment), do: [url] + defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url] + defp add_in_reply_to(_), do: [] + + defp add_event_author(nil), do: [] + + defp add_event_author(%Event{} = event) do + [Mobilizon.Storage.Repo.preload(event, [:organizer_actor]).organizer_actor.url] + end + + defp add_comment_author(nil), do: nil + + defp add_comment_author(%Comment{} = comment) do + case Mobilizon.Storage.Repo.preload(comment, [:actor]) do + %Comment{actor: %Actor{url: url}} -> + url + + _err -> + nil + end + end + + defp add_comments_authors(comments) do + authors = + comments + |> Enum.map(&add_comment_author/1) + |> Enum.filter(& &1) + + authors + end + + @spec add_shares_actors_followers(String.t()) :: list(String.t()) + defp add_shares_actors_followers(uri) do + uri + |> Share.get_actors_by_share_uri() + |> Enum.map(&Actors.list_followers_actors_for_actor/1) + |> List.flatten() + |> Enum.map(& &1.url) + |> Enum.uniq() + end + + defp add_actors_that_had_our_content(actor_id) do + actor_id + |> Share.get_actors_by_owner_actor_id() + |> Enum.map(&Actors.list_followers_actors_for_actor/1) + |> List.flatten() + |> Enum.map(& &1.url) + |> Enum.uniq() + end + + defp process_mention({_, mentioned_actor}), do: mentioned_actor.url + + defp process_mention(%{actor_id: actor_id}) do + with %Actor{url: url} <- Actors.get_actor(actor_id) do + url + end + end end diff --git a/lib/service/activity_pub/converter/actor.ex b/lib/service/activity_pub/converter/actor.ex index 3a0c4546e..556a4d8b9 100644 --- a/lib/service/activity_pub/converter/actor.ex +++ b/lib/service/activity_pub/converter/actor.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do """ alias Mobilizon.Actors.Actor, as: ActorModel - alias Mobilizon.Service.ActivityPub.{Converter, Convertible} + alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils} @behaviour Converter @@ -22,33 +22,40 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do """ @impl Converter @spec as_to_model_data(map) :: map - def as_to_model_data(object) do + def as_to_model_data(data) do avatar = - object["icon"]["url"] && + data["icon"]["url"] && %{ - "name" => object["icon"]["name"] || "avatar", - "url" => object["icon"]["url"] + "name" => data["icon"]["name"] || "avatar", + "url" => MobilizonWeb.MediaProxy.url(data["icon"]["url"]) } banner = - object["image"]["url"] && + data["image"]["url"] && %{ - "name" => object["image"]["name"] || "banner", - "url" => object["image"]["url"] + "name" => data["image"]["name"] || "banner", + "url" => MobilizonWeb.MediaProxy.url(data["image"]["url"]) } - {:ok, - %{ - "type" => String.to_existing_atom(object["type"]), - "preferred_username" => object["preferredUsername"], - "summary" => object["summary"], - "url" => object["id"], - "name" => object["name"], - "avatar" => avatar, - "banner" => banner, - "keys" => object["publicKey"]["publicKeyPem"], - "manually_approves_followers" => object["manuallyApprovesFollowers"] - }} + actor_data = %{ + url: data["id"], + avatar: avatar, + banner: banner, + name: data["name"], + preferred_username: data["preferredUsername"], + summary: data["summary"], + keys: data["publicKey"]["publicKeyPem"], + inbox_url: data["inbox"], + outbox_url: data["outbox"], + following_url: data["following"], + followers_url: data["followers"], + shared_inbox_url: data["endpoints"]["sharedInbox"], + domain: URI.parse(data["id"]).host, + manually_approves_followers: data["manuallyApprovesFollowers"], + type: data["type"] + } + + {:ok, actor_data} end @doc """ @@ -57,18 +64,51 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do @impl Converter @spec model_to_as(ActorModel.t()) :: map def model_to_as(%ActorModel{} = actor) do - %{ - "type" => Atom.to_string(actor.type), - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "preferred_username" => actor.preferred_username, + actor_data = %{ + "id" => actor.url, + "type" => actor.type, + "preferredUsername" => actor.preferred_username, "name" => actor.name, "summary" => actor.summary, - "following" => ActorModel.build_url(actor.preferred_username, :following), - "followers" => ActorModel.build_url(actor.preferred_username, :followers), - "inbox" => ActorModel.build_url(actor.preferred_username, :inbox), - "outbox" => ActorModel.build_url(actor.preferred_username, :outbox), - "id" => ActorModel.build_url(actor.preferred_username, :page), - "url" => actor.url + "following" => actor.following_url, + "followers" => actor.followers_url, + "inbox" => actor.inbox_url, + "outbox" => actor.outbox_url, + "url" => actor.url, + "endpoints" => %{ + "sharedInbox" => actor.shared_inbox_url + }, + "manuallyApprovesFollowers" => actor.manually_approves_followers, + "publicKey" => %{ + "id" => "#{actor.url}#main-key", + "owner" => actor.url, + "publicKeyPem" => + if(is_nil(actor.domain) and not is_nil(actor.keys), + do: Utils.pem_to_public_key_pem(actor.keys), + else: actor.keys + ) + } } + + actor_data = + if is_nil(actor.avatar) do + actor_data + else + Map.put(actor_data, "icon", %{ + "type" => "Image", + "mediaType" => actor.avatar.content_type, + "url" => actor.avatar.url + }) + end + + if is_nil(actor.banner) do + actor_data + else + Map.put(actor_data, "image", %{ + "type" => "Image", + "mediaType" => actor.banner.content_type, + "url" => actor.banner.url + }) + end end end diff --git a/lib/service/activity_pub/converter/comment.ex b/lib/service/activity_pub/converter/comment.ex index da73e53c1..3875deb0a 100644 --- a/lib/service/activity_pub/converter/comment.ex +++ b/lib/service/activity_pub/converter/comment.ex @@ -12,6 +12,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.{Converter, Convertible} alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils + alias Mobilizon.Tombstone, as: TombstoneModel require Logger @@ -32,9 +33,11 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do Logger.debug("We're converting raw ActivityStream data to a comment entity") Logger.debug(inspect(object)) - with {:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(object["actor"]), - {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])}, - {:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])} do + with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"), + {:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(author_url), + {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))}, + {:mentions, mentions} <- + {:mentions, ConverterUtils.fetch_mentions(Map.get(object, "tag", []))} do Logger.debug("Inserting full comment") Logger.debug(inspect(object)) @@ -70,6 +73,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do 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} -> @@ -106,6 +110,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do "to" => to, "cc" => [], "content" => comment.text, + "mediaType" => "text/html", "actor" => comment.actor.url, "attributedTo" => comment.actor.url, "uuid" => comment.uuid, @@ -114,23 +119,27 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags) } - if comment.in_reply_to_comment do - object |> Map.put("inReplyTo", comment.in_reply_to_comment.url || comment.event.url) - else - object + cond do + comment.in_reply_to_comment -> + Map.put(object, "inReplyTo", comment.in_reply_to_comment.url) + + comment.event -> + Map.put(object, "inReplyTo", comment.event.url) + + true -> + object end end @impl Converter @spec model_to_as(CommentModel.t()) :: map + @doc """ + A "soft-deleted" comment is a tombstone + """ def model_to_as(%CommentModel{} = comment) do - %{ - "type" => "Tombstone", - "uuid" => comment.uuid, - "id" => comment.url, - "published" => comment.inserted_at, - "updated" => comment.updated_at, - "deleted" => comment.deleted_at - } + Convertible.model_to_as(%TombstoneModel{ + uri: comment.url, + inserted_at: comment.deleted_at + }) end end diff --git a/lib/service/activity_pub/converter/event.ex b/lib/service/activity_pub/converter/event.ex index e5c40e227..a273b87f1 100644 --- a/lib/service/activity_pub/converter/event.ex +++ b/lib/service/activity_pub/converter/event.ex @@ -6,14 +6,13 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do internal one, and back. """ - alias Mobilizon.{Addresses, Media} + alias Mobilizon.Addresses alias Mobilizon.Actors.Actor alias Mobilizon.Addresses.Address alias Mobilizon.Events.Event, as: EventModel - alias Mobilizon.Events.EventOptions alias Mobilizon.Media.Picture alias Mobilizon.Service.ActivityPub - alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils} + alias Mobilizon.Service.ActivityPub.{Converter, Convertible} alias Mobilizon.Service.ActivityPub.Converter.Address, as: AddressConverter alias Mobilizon.Service.ActivityPub.Converter.Picture, as: PictureConverter alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils @@ -37,26 +36,25 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do Logger.debug("event as_to_model_data") Logger.debug(inspect(object)) - with {:actor, {:ok, %Actor{id: actor_id}}} <- - {:actor, ActivityPub.get_or_fetch_actor_by_url(object["actor"])}, + with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"), + {:actor, {:ok, %Actor{id: actor_id, domain: actor_domain}}} <- + {:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)}, {:address, address_id} <- {:address, get_address(object["location"])}, {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])}, + {:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])}, {:visibility, visibility} <- {:visibility, get_visibility(object)}, {:options, options} <- {:options, get_options(object)} do picture_id = with true <- Map.has_key?(object, "attachment") && length(object["attachment"]) > 0, - %Picture{id: picture_id} <- - Media.get_picture_by_url( - object["attachment"] - |> hd - |> Map.get("url") - |> hd - |> Map.get("href") - ) do + {:ok, %Picture{id: picture_id}} <- + object["attachment"] + |> hd + |> PictureConverter.find_or_create_picture(actor_id) do picture_id else - _ -> nil + _err -> + nil end entity = %{ @@ -68,16 +66,20 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do ends_on: object["endTime"], category: object["category"], visibility: visibility, - join_options: Map.get(object, "joinOptions", "free"), + join_options: Map.get(object, "joinMode", "free"), + local: is_nil(actor_domain), options: options, - status: object["status"], + status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(), online_address: object["onlineAddress"], phone_address: object["phoneAddress"], - draft: object["draft"] || false, + draft: false, url: object["id"], uuid: object["uuid"], tags: tags, - physical_address_id: address_id + mentions: mentions, + physical_address_id: address_id, + updated_at: object["updated"], + publish_at: object["published"] } {:ok, entity} @@ -108,14 +110,17 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do "uuid" => event.uuid, "category" => event.category, "content" => event.description, - "publish_at" => (event.publish_at || event.inserted_at) |> date_to_string(), - "updated_at" => event.updated_at |> date_to_string(), + "published" => (event.publish_at || event.inserted_at) |> date_to_string(), + "updated" => event.updated_at |> date_to_string(), "mediaType" => "text/html", "startTime" => event.begins_on |> date_to_string(), - "joinOptions" => to_string(event.join_options), + "joinMode" => to_string(event.join_options), "endTime" => event.ends_on |> date_to_string(), "tag" => event.tags |> ConverterUtils.build_tags(), - "draft" => event.draft, + "maximumAttendeeCapacity" => event.options.maximum_attendee_capacity, + "repliesModerationOption" => event.options.comment_moderation, + # "draft" => event.draft, + "ical:status" => event.status |> to_string |> String.upcase(), "id" => event.url, "url" => event.url } @@ -133,17 +138,10 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do # Get only elements that we have in EventOptions @spec get_options(map) :: map defp get_options(object) do - keys = - EventOptions - |> struct - |> Map.keys() - |> List.delete(:__struct__) - |> Enum.map(&Utils.camelize/1) - - Enum.reduce(object, %{}, fn {key, value}, acc -> - (!is_nil(value) && key in keys && Map.put(acc, Utils.underscore(key), value)) || - acc - end) + %{ + maximum_attendee_capacity: object["maximumAttendeeCapacity"], + comment_moderation: object["repliesModerationOption"] + } end @spec get_address(map | binary | nil) :: integer | nil @@ -186,13 +184,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do @ap_public "https://www.w3.org/ns/activitystreams#Public" - defp get_visibility(object) do - cond do - @ap_public in object["to"] -> :public - @ap_public in object["cc"] -> :unlisted - true -> :private - end - end + defp get_visibility(object), do: if(@ap_public in object["to"], do: :public, else: :unlisted) @spec date_to_string(DateTime.t() | nil) :: String.t() defp date_to_string(nil), do: nil diff --git a/lib/service/activity_pub/converter/flag.ex b/lib/service/activity_pub/converter/flag.ex index ac2c992b3..2980b0c69 100644 --- a/lib/service/activity_pub/converter/flag.ex +++ b/lib/service/activity_pub/converter/flag.ex @@ -15,6 +15,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Flag do alias Mobilizon.Reports.Report alias Mobilizon.Service.ActivityPub.Converter alias Mobilizon.Service.ActivityPub.Convertible + alias Mobilizon.Service.ActivityPub.Relay @behaviour Converter @@ -42,8 +43,6 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Flag do end end - @audience %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []} - @doc """ Convert an event struct to an ActivityStream representation """ @@ -54,17 +53,13 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Flag do object = if report.event, do: object ++ [report.event.url], else: object - audience = - if report.local, do: @audience, else: Map.put(@audience, "cc", [report.reported.url]) - %{ "type" => "Flag", - "actor" => report.reporter.url, + "actor" => Relay.get_actor().url, "id" => report.url, "content" => report.content, "object" => object } - |> Map.merge(audience) end @spec as_to_model(map) :: map @@ -91,7 +86,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Flag do end end), - # Remove the reported user from the object list. + # Remove the reported actor and the event from the object list. comments <- Enum.filter(objects, fn url -> !(url == reported.url || (!is_nil(event) && event.url == url)) diff --git a/lib/service/activity_pub/converter/picture.ex b/lib/service/activity_pub/converter/picture.ex index 6c6e93ab6..292db9bda 100644 --- a/lib/service/activity_pub/converter/picture.ex +++ b/lib/service/activity_pub/converter/picture.ex @@ -15,14 +15,48 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Picture do def model_to_as(%PictureModel{file: file}) do %{ "type" => "Document", - "url" => [ - %{ - "type" => "Link", - "mediaType" => file.content_type, - "href" => file.url - } - ], + "mediaType" => file.content_type, + "url" => file.url, "name" => file.name } end + + @doc """ + Save picture data from raw data and return AS Link data. + """ + def find_or_create_picture(%{"type" => "Link", "href" => url}, actor_id), + do: find_or_create_picture(url, actor_id) + + def find_or_create_picture( + %{"type" => "Document", "url" => picture_url, "name" => name}, + actor_id + ) + when is_bitstring(picture_url) do + with {:ok, %HTTPoison.Response{body: body}} <- HTTPoison.get(picture_url), + {:ok, + %{ + name: name, + url: url, + content_type: content_type, + size: size + }} <- + MobilizonWeb.Upload.store(%{body: body, name: name}), + {:picture_exists, nil} <- {:picture_exists, Mobilizon.Media.get_picture_by_url(url)} do + Mobilizon.Media.create_picture(%{ + "file" => %{ + "url" => url, + "name" => name, + "content_type" => content_type, + "size" => size + }, + "actor_id" => actor_id + }) + else + {:picture_exists, %PictureModel{file: _file} = picture} -> + {:ok, picture} + + err -> + err + end + end end diff --git a/lib/service/activity_pub/converter/tombstone.ex b/lib/service/activity_pub/converter/tombstone.ex new file mode 100644 index 000000000..8054ae376 --- /dev/null +++ b/lib/service/activity_pub/converter/tombstone.ex @@ -0,0 +1,40 @@ +defmodule Mobilizon.Service.ActivityPub.Converter.Tombstone do + @moduledoc """ + Comment converter. + + This module allows to convert Tombstone models to ActivityStreams data + """ + + alias Mobilizon.Tombstone, as: TombstoneModel + alias Mobilizon.Service.ActivityPub.{Converter, Convertible} + + require Logger + + @behaviour Converter + + defimpl Convertible, for: TombstoneModel do + alias Mobilizon.Service.ActivityPub.Converter.Tombstone, as: TombstoneConverter + + defdelegate model_to_as(comment), to: TombstoneConverter + end + + @doc """ + Make an AS tombstone object from an existing `Tombstone` structure. + """ + @impl Converter + @spec model_to_as(TombstoneModel.t()) :: map + def model_to_as(%TombstoneModel{} = tombstone) do + %{ + "type" => "Tombstone", + "id" => tombstone.uri, + "deleted" => tombstone.inserted_at + } + end + + @doc """ + Converting an Tombstone to an object makes no sense, nevertheless… + """ + @impl Converter + @spec as_to_model_data(map) :: map + def as_to_model_data(object), do: object +end diff --git a/lib/service/activity_pub/converter/utils.ex b/lib/service/activity_pub/converter/utils.ex index ba2f3a2af..8e637736f 100644 --- a/lib/service/activity_pub/converter/utils.ex +++ b/lib/service/activity_pub/converter/utils.ex @@ -14,6 +14,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Utils do @spec fetch_tags([String.t()]) :: [Tag.t()] def fetch_tags(tags) when is_list(tags) do Logger.debug("fetching tags") + Logger.debug(inspect(tags)) tags |> Enum.flat_map(&fetch_tag/1) |> Enum.uniq() |> Enum.map(&existing_tag_or_data/1) end @@ -64,6 +65,8 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Utils do } end + defp fetch_tag(%{title: title}), do: [title] + defp fetch_tag(tag) when is_map(tag) do case tag["type"] do "Hashtag" -> diff --git a/lib/service/activity_pub/relay.ex b/lib/service/activity_pub/relay.ex index 90c2ab1ad..eb45e0d10 100644 --- a/lib/service/activity_pub/relay.ex +++ b/lib/service/activity_pub/relay.ex @@ -9,27 +9,37 @@ defmodule Mobilizon.Service.ActivityPub.Relay do """ alias Mobilizon.Actors - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.{Activity, Transmogrifier} + alias Mobilizon.Service.WebFinger alias MobilizonWeb.API.Follows require Logger + def init() do + # Wait for everything to settle. + Process.sleep(1000 * 5) + get_actor() + end + + @spec get_actor() :: Actor.t() | {:error, Ecto.Changeset.t()} def get_actor do with {:ok, %Actor{} = actor} <- - Actors.get_or_create_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do + Actors.get_or_create_instance_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do actor end end - def follow(target_instance) do - with %Actor{} = local_actor <- get_actor(), + @spec follow(String.t()) :: {:ok, Activity.t(), Follower.t()} + def follow(address) do + with {:ok, target_instance} <- fetch_actor(address), + %Actor{} = local_actor <- get_actor(), {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance), - {:ok, activity} <- Follows.follow(local_actor, target_actor) do + {:ok, activity, follow} <- Follows.follow(local_actor, target_actor) do Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}") - {:ok, activity} + {:ok, activity, follow} else e -> Logger.warn("Error while following remote instance: #{inspect(e)}") @@ -37,12 +47,14 @@ defmodule Mobilizon.Service.ActivityPub.Relay do end end - def unfollow(target_instance) do - with %Actor{} = local_actor <- get_actor(), + @spec unfollow(String.t()) :: {:ok, Activity.t(), Follower.t()} + def unfollow(address) do + with {:ok, target_instance} <- fetch_actor(address), + %Actor{} = local_actor <- get_actor(), {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance), - {:ok, activity} <- Follows.unfollow(local_actor, target_actor) do + {:ok, activity, follow} <- Follows.unfollow(local_actor, target_actor) do Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}") - {:ok, activity} + {:ok, activity, follow} else e -> Logger.warn("Error while unfollowing remote instance: #{inspect(e)}") @@ -50,30 +62,38 @@ defmodule Mobilizon.Service.ActivityPub.Relay do end end - def accept(target_instance) do - with %Actor{} = local_actor <- get_actor(), + @spec accept(String.t()) :: {:ok, Activity.t(), Follower.t()} + def accept(address) do + Logger.debug("We're trying to accept a relay subscription") + + with {:ok, target_instance} <- fetch_actor(address), + %Actor{} = local_actor <- get_actor(), {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance), - {:ok, activity} <- Follows.accept(target_actor, local_actor) do - {:ok, activity} + {:ok, activity, follow} <- Follows.accept(target_actor, local_actor) do + {:ok, activity, follow} end end - # def reject(target_instance) do - # with %Actor{} = local_actor <- get_actor(), - # {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_actor_by_url(target_instance), - # {:ok, activity} <- Follows.reject(target_actor, local_actor) do - # {:ok, activity} - # end - # end + def reject(address) do + Logger.debug("We're trying to reject a relay subscription") + + with {:ok, target_instance} <- fetch_actor(address), + %Actor{} = local_actor <- get_actor(), + {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance), + {:ok, activity, follow} <- Follows.reject(target_actor, local_actor) do + {:ok, activity, follow} + end + end @doc """ Publish an activity to all relays following this instance """ def publish(%Activity{data: %{"object" => object}} = _activity) do with %Actor{id: actor_id} = actor <- get_actor(), - {:ok, object} <- - Transmogrifier.fetch_obj_helper_as_activity_streams(object) do - ActivityPub.announce(actor, object, "#{object["id"]}/announces/#{actor_id}", true, false) + {object, object_id} <- fetch_object(object), + id <- "#{object_id}/announces/#{actor_id}" do + Logger.info("Publishing activity #{id} to all relays") + ActivityPub.announce(actor, object, id, true, false) else e -> Logger.error("Error while getting local instance actor: #{inspect(e)}") @@ -85,4 +105,51 @@ defmodule Mobilizon.Service.ActivityPub.Relay do Logger.debug(inspect(err)) nil end + + defp fetch_object(object) when is_map(object) do + with {:ok, object} <- Transmogrifier.fetch_obj_helper_as_activity_streams(object) do + {object, object["id"]} + end + end + + defp fetch_object(object) when is_bitstring(object), do: {object, object} + + @spec fetch_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()} + # Dirty hack + defp fetch_actor("https://" <> address), do: fetch_actor(address) + defp fetch_actor("http://" <> address), do: fetch_actor(address) + + defp fetch_actor(address) do + %URI{host: host} = URI.parse("http://" <> address) + + cond do + String.contains?(address, "@") -> + check_actor(address) + + !is_nil(host) -> + check_actor("relay@#{host}") + + true -> + {:error, "Bad URL"} + end + end + + @spec check_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()} + defp check_actor(username_and_domain) do + case Actors.get_actor_by_name(username_and_domain) do + %Actor{url: url} -> {:ok, url} + nil -> finger_actor(username_and_domain) + end + end + + @spec finger_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()} + defp finger_actor(nickname) do + case WebFinger.finger(nickname) do + {:ok, %{"url" => url}} when not is_nil(url) -> + {:ok, url} + + _e -> + {:error, "No ActivityPub URL found in WebFinger"} + end + end end diff --git a/lib/service/activity_pub/transmogrifier.ex b/lib/service/activity_pub/transmogrifier.ex index b82b3fbe2..d21d965da 100644 --- a/lib/service/activity_pub/transmogrifier.ex +++ b/lib/service/activity_pub/transmogrifier.ex @@ -20,108 +20,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do require Logger - def get_actor(%{"actor" => actor}) when is_binary(actor) do - actor - end - - def get_actor(%{"actor" => actor}) when is_list(actor) do - if is_binary(Enum.at(actor, 0)) do - Enum.at(actor, 0) - else - actor - |> Enum.find(fn %{"type" => type} -> type in ["Person", "Service", "Application"] end) - |> Map.get("id") - end - end - - def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do - id - end - - def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do - get_actor(%{"actor" => actor}) - end - - @doc """ - Modifies an incoming AP object (mastodon format) to our internal format. - """ - def fix_object(object) do - object - |> Map.put("actor", object["attributedTo"]) - |> fix_attachments - - # |> fix_in_reply_to - - # |> fix_tag - end - - def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) - when not is_nil(in_reply_to) and is_bitstring(in_reply_to) do - in_reply_to |> do_fix_in_reply_to(object) - end - - def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) - when not is_nil(in_reply_to) and is_map(in_reply_to) do - if is_bitstring(in_reply_to["id"]) do - in_reply_to["id"] |> do_fix_in_reply_to(object) - end - end - - def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) - when not is_nil(in_reply_to) and is_list(in_reply_to) do - if is_bitstring(Enum.at(in_reply_to, 0)) do - in_reply_to |> Enum.at(0) |> do_fix_in_reply_to(object) - end - end - - def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) - when not is_nil(in_reply_to) do - Logger.warn("inReplyTo ID seem incorrect: #{inspect(in_reply_to)}") - do_fix_in_reply_to("", object) - end - - def fix_in_reply_to(object), do: object - - def do_fix_in_reply_to(in_reply_to_id, object) do - case fetch_obj_helper(in_reply_to_id) do - {:ok, replied_object} -> - object - |> Map.put("inReplyTo", replied_object.url) - - {:error, {:error, :not_supported}} -> - Logger.info("Object reply origin has not a supported type") - object - - e -> - Logger.warn("Couldn't fetch #{in_reply_to_id} #{inspect(e)}") - object - end - end - - def fix_attachments(object) do - attachments = - (object["attachment"] || []) - |> Enum.map(fn data -> - url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}] - Map.put(data, "url", url) - end) - - object - |> Map.put("attachment", attachments) - end - - def fix_tag(object) do - tags = - (object["tag"] || []) - |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end) - |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end) - - combined = (object["tag"] || []) ++ tags - - object - |> Map.put("tag", combined) - end - def handle_incoming(%{"id" => nil}), do: :error def handle_incoming(%{"id" => ""}), do: :error @@ -135,6 +33,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do additional: %{ "cc" => [params["reported"].url] }, + event_id: if(is_nil(params["event"]), do: nil, else: params["event"].id || nil), local: false } @@ -158,7 +57,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do Logger.info("Handle incoming to create notes") with {:ok, object_data} <- - object |> fix_object() |> Converter.Comment.as_to_model_data(), + object |> Converter.Comment.as_to_model_data(), {:existing_comment, {:error, :comment_not_found}} <- {:existing_comment, Events.get_comment_from_url_with_preload(object_data.url)}, {:ok, %Activity{} = activity, %Comment{} = comment} <- @@ -186,7 +85,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do Logger.info("Handle incoming to create event") with {:ok, object_data} <- - object |> fix_object() |> Converter.Event.as_to_model_data(), + object |> Converter.Event.as_to_model_data(), {:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)}, {:ok, %Activity{} = activity, %Event{} = event} <- ActivityPub.create(:event, object_data, false) do @@ -273,36 +172,25 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do end end - # - # def handle_incoming( - # %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data - # ) do - # with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - # {:ok, object} <- - # fetch_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), - # {:ok, activity, object} <- ActivityPub.like(actor, object, id, false) do - # {:ok, activity} - # else - # _e -> :error - # end - # end - # # def handle_incoming( - %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => _id} = data + %{"type" => "Announce", "object" => object, "actor" => _actor, "id" => _id} = data ) do with actor <- get_actor(data), # TODO: Is the following line useful? - {:ok, %Actor{} = _actor} <- ActivityPub.get_or_fetch_actor_by_url(actor), + {:ok, %Actor{id: actor_id} = _actor} <- ActivityPub.get_or_fetch_actor_by_url(actor), :ok <- Logger.debug("Fetching contained object"), - {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id), + {:ok, object} <- fetch_obj_helper_as_activity_streams(object), :ok <- Logger.debug("Handling contained object"), create_data <- make_create_data(object), :ok <- Logger.debug(inspect(object)), - {:ok, _activity, object} <- handle_incoming(create_data), + {:ok, _activity, entity} <- handle_incoming(create_data), :ok <- Logger.debug("Finished processing contained object"), - {:ok, activity} <- ActivityPub.create_activity(data, false) do - {:ok, activity, object} + {:ok, activity} <- ActivityPub.create_activity(data, false), + {:ok, %Actor{id: object_owner_actor_id}} <- Actors.get_actor_by_url(object["actor"]), + {:ok, %Mobilizon.Share{} = _share} <- + Mobilizon.Share.create(object["id"], actor_id, object_owner_actor_id) do + {:ok, activity, entity} else e -> Logger.debug(inspect(e)) @@ -318,7 +206,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do when object_type in ["Person", "Group", "Application", "Service", "Organization"] do with {:ok, %Actor{} = old_actor} <- Actors.get_actor_by_url(object["id"]), {:ok, object_data} <- - object |> fix_object() |> Converter.Actor.as_to_model_data(), + object |> Converter.Actor.as_to_model_data(), {:ok, %Activity{} = activity, %Actor{} = new_actor} <- ActivityPub.update(:actor, old_actor, object_data, false) do {:ok, activity, new_actor} @@ -331,12 +219,15 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => _actor} = - _update + update_data ) do - with %Event{} = old_event <- - Events.get_event_by_url(object["id"]), + with actor <- get_actor(update_data), + {:ok, %Actor{url: actor_url}} <- Actors.get_actor_by_url(actor), + {:ok, %Event{} = old_event} <- + object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), {:ok, object_data} <- - object |> fix_object() |> Converter.Event.as_to_model_data(), + object |> Converter.Event.as_to_model_data(), + {:origin_check, true} <- {:origin_check, origin_check?(actor_url, update_data)}, {:ok, %Activity{} = activity, %Event{} = new_event} <- ActivityPub.update(:event, old_event, object_data, false) do {:ok, activity, new_event} @@ -396,16 +287,18 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data ) do - object_id = Utils.get_url(object) - with actor <- get_actor(data), - {:ok, %Actor{url: _actor_url}} <- Actors.get_actor_by_url(actor), - {:ok, object} <- fetch_obj_helper(object_id), - # TODO : Validate that DELETE comes indeed form right domain (see above) - # :ok <- contain_origin(actor_url, object.data), + {:ok, %Actor{url: actor_url}} <- Actors.get_actor_by_url(actor), + object_id <- Utils.get_url(object), + {:origin_check, true} <- {:origin_check, 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 {:ok, activity, object} else + {:origin_check, false} -> + Logger.warn("Object origin check failed") + :error + e -> Logger.debug(inspect(e)) :error @@ -413,12 +306,13 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Join", "object" => object, "actor" => _actor, "id" => _id} = data + %{"type" => "Join", "object" => object, "actor" => _actor, "id" => id} = data ) do with actor <- get_actor(data), {:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor), - {:ok, object} <- fetch_obj_helper(object), - {:ok, activity, object} <- ActivityPub.join(object, actor, false) do + object <- Utils.get_url(object), + {:ok, object} <- ActivityPub.fetch_object_from_url(object), + {:ok, activity, object} <- ActivityPub.join(object, actor, false, %{url: id}) do {:ok, activity, object} else e -> @@ -432,7 +326,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do ) do with actor <- get_actor(data), {:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor), - {:ok, object} <- fetch_obj_helper(object), + object <- Utils.get_url(object), + {:ok, object} <- ActivityPub.fetch_object_from_url(object), {:ok, activity, object} <- ActivityPub.leave(object, actor, false) do {:ok, activity, object} else @@ -487,7 +382,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do ActivityPub.accept( :follow, follow, - %{approved: true}, false ) do {:ok, activity, follow} @@ -511,23 +405,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do Handle incoming `Reject` activities wrapping a `Follow` activity """ def do_handle_incoming_reject_following(follow_object, %Actor{} = actor) do - with {:follow, - {:ok, - %Follower{approved: false, actor: follower, id: follow_id, target_actor: followed} = - follow}} <- + with {:follow, {:ok, %Follower{approved: false, target_actor: followed} = follow}} <- {:follow, get_follow(follow_object)}, {:same_actor, true} <- {:same_actor, actor.id == followed.id}, {:ok, activity, _} <- - ActivityPub.reject( - %{ - to: [follower.url], - actor: actor.url, - object: follow_object, - local: false - }, - "#{MobilizonWeb.Endpoint.url()}/reject/follow/#{follow_id}" - ), - {:ok, %Follower{}} <- Actors.delete_follower(follow) do + ActivityPub.reject(:follow, follow) do {:ok, activity, follow} else {:follow, _} -> @@ -547,7 +429,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do # Handle incoming `Accept` activities wrapping a `Join` activity on an event defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do - with {:join_event, {:ok, %Participant{role: :not_approved, event: event} = participant}} <- + with {:join_event, {:ok, %Participant{role: role, event: event} = participant}} + when role in [:not_approved, :rejected] <- {:join_event, get_participant(join_object)}, # TODO: The actor that accepts the Join activity may another one that the event organizer ? # Or maybe for groups it's the group that sends the Accept activity @@ -556,7 +439,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do ActivityPub.accept( :join, participant, - %{role: :participant}, false ), :ok <- @@ -587,32 +469,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do # Handle incoming `Reject` activities wrapping a `Join` activity on an event defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do - with {:join_event, - {:ok, - %Participant{role: :not_approved, actor: actor, id: join_id, event: event} = - participant}} <- + with {:join_event, {:ok, %Participant{event: event, role: role} = participant}} + when role != :rejected <- {:join_event, get_participant(join_object)}, # TODO: The actor that accepts the Join activity may another one that the event organizer ? # Or maybe for groups it's the group that sends the Accept activity {:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id}, - {:ok, activity, _} <- - ActivityPub.reject( - %{ - to: [actor.url], - actor: actor_accepting.url, - object: join_object, - local: false - }, - "#{MobilizonWeb.Endpoint.url()}/reject/join/#{join_id}" - ), - {:ok, %Participant{role: :rejected} = participant} <- - Events.update_participant(participant, %{"role" => :rejected}), + {:ok, activity, participant} <- + ActivityPub.reject(:join, participant, false), :ok <- Participation.send_emails_to_local_user(participant) do {:ok, activity, participant} else - {:join_event, {:ok, %Participant{role: :participant}}} -> - Logger.debug( - "Tried to handle an Reject activity on a Join activity with a event object but the participant is already validated" + {:join_event, {:ok, %Participant{role: :rejected}}} -> + Logger.warn( + "Tried to handle an Reject activity on a Join activity with a event object but the participant is already rejected" ) nil @@ -662,49 +532,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do end end - def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) do - with false <- String.starts_with?(in_reply_to, "http"), - {:ok, replied_to_object} <- fetch_obj_helper(in_reply_to) do - Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to) - else - _e -> object - end - end - - def set_reply_to_uri(obj), do: obj - # - # # Prepares the object of an outgoing create activity. - def prepare_object(object) do - object - # |> set_sensitive - # |> add_hashtags - |> add_mention_tags - # |> add_emoji_tags - |> add_attributed_to - # |> prepare_attachments - |> set_reply_to_uri - end - - @doc """ - internal -> Mastodon - """ - def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do - Logger.debug("Prepare outgoing for a note creation") - - object = - object - |> prepare_object - - data = - data - |> Map.put("object", object) - |> Map.merge(Utils.make_json_ld_header()) - - Logger.debug("Finished prepare outgoing for a note creation") - - {:ok, data} - end - def prepare_outgoing(%{"type" => _type} = data) do data = data @@ -713,145 +540,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do {:ok, data} end - # def prepare_outgoing(%Event{} = event) do - # event = - # event - # |> Map.from_struct() - # |> Map.drop([:__meta__]) - # |> Map.put(:"@context", "https://www.w3.org/ns/activitystreams") - # |> prepare_object - - # {:ok, event} - # end - - # def prepare_outgoing(%Comment{} = comment) do - # comment = - # comment - # |> Map.from_struct() - # |> Map.drop([:__meta__]) - # |> Map.put(:"@context", "https://www.w3.org/ns/activitystreams") - # |> prepare_object - - # {:ok, comment} - # end - - # - # def maybe_fix_object_url(data) do - # if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do - # case ActivityPub.fetch_object_from_id(data["object"]) do - # {:ok, relative_object} -> - # if relative_object.data["external_url"] do - # data = - # data - # |> Map.put("object", relative_object.data["external_url"]) - # else - # data - # end - # - # e -> - # Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}") - # data - # end - # else - # data - # end - # end - # - - def add_hashtags(object) do - tags = - (object["tag"] || []) - |> Enum.map(fn tag -> - %{ - "href" => MobilizonWeb.Endpoint.url() <> "/tags/#{tag}", - "name" => "##{tag}", - "type" => "Hashtag" - } - end) - - object - |> Map.put("tag", tags) - end - - def add_mention_tags(object) do - Logger.debug("add mention tags") - Logger.debug(inspect(object)) - - recipients = - (object["to"] ++ (object["cc"] || [])) -- ["https://www.w3.org/ns/activitystreams#Public"] - - mentions = - recipients - |> Enum.filter(& &1) - |> Enum.map(fn url -> - case Actors.get_actor_by_url(url) do - {:ok, actor} -> actor - _ -> nil - end - end) - |> Enum.filter(& &1) - |> Enum.map(fn actor -> - %{ - "type" => "Mention", - "href" => actor.url, - "name" => "@#{Actor.preferred_username_and_domain(actor)}" - } - end) - - tags = object["tag"] || [] - - object - |> Map.put("tag", tags ++ mentions) - end - - # - # # TODO: we should probably send mtime instead of unix epoch time for updated - # def add_emoji_tags(object) do - # tags = object["tag"] || [] - # emoji = object["emoji"] || [] - # - # out = - # emoji - # |> Enum.map(fn {name, url} -> - # %{ - # "icon" => %{"url" => url, "type" => "Image"}, - # "name" => ":" <> name <> ":", - # "type" => "Emoji", - # "updated" => "1970-01-01T00:00:00Z", - # "id" => url - # } - # end) - # - # object - # |> Map.put("tag", tags ++ out) - # end - # - - # - # def set_sensitive(object) do - # tags = object["tag"] || [] - # Map.put(object, "sensitive", "nsfw" in tags) - # end - # - def add_attributed_to(object) do - attributed_to = object["attributedTo"] || object["actor"] - - object |> Map.put("attributedTo", attributed_to) - end - - # - # def prepare_attachments(object) do - # attachments = - # (object["attachment"] || []) - # |> Enum.map(fn data -> - # [%{"mediaType" => media_type, "href" => href} | _] = data["url"] - # %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"} - # end) - # - # object - # |> Map.put("attachment", attachments) - # end - @spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any() def fetch_obj_helper(object) do Logger.debug("fetch_obj_helper") @@ -862,7 +550,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do {:ok, object} err -> - Logger.info("Error while fetching #{inspect(object)}") + Logger.warn("Error while fetching #{inspect(object)}") {:error, err} end end diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index cc79aeb99..94c06a16a 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -8,20 +8,11 @@ defmodule Mobilizon.Service.ActivityPub.Utils do # Various ActivityPub related utils. """ - alias Ecto.Changeset - - alias Mobilizon.{Actors, Addresses, Events, Reports, Users} + alias Mobilizon.Actors alias Mobilizon.Actors.Actor - alias Mobilizon.Addresses.Address - alias Mobilizon.Events.{Comment, Event} alias Mobilizon.Media.Picture - alias Mobilizon.Reports.Report alias Mobilizon.Service.ActivityPub.{Activity, Converter} alias Mobilizon.Service.Federator - alias Mobilizon.Storage.Repo - - alias MobilizonWeb.{Email, Endpoint} - alias MobilizonWeb.Router.Helpers, as: Routes require Logger @@ -37,12 +28,31 @@ defmodule Mobilizon.Service.ActivityPub.Utils do %{ "@context" => [ "https://www.w3.org/ns/activitystreams", - "https://litepub.github.io/litepub/context.jsonld", + "https://litepub.social/litepub/context.jsonld", %{ "sc" => "http://schema.org#", + "ical" => "http://www.w3.org/2002/12/cal/ical#", "Hashtag" => "as:Hashtag", "category" => "sc:category", - "uuid" => "sc:identifier" + "uuid" => "sc:identifier", + "maximumAttendeeCapacity" => "sc:maximumAttendeeCapacity", + "mz" => "https://joinmobilizon.org/ns#", + "repliesModerationOptionType" => %{ + "@id" => "mz:repliesModerationOptionType", + "@type" => "rdfs:Class" + }, + "repliesModerationOption" => %{ + "@id" => "mz:repliesModerationOption", + "@type" => "mz:repliesModerationOptionType" + }, + "joinModeType" => %{ + "@id" => "mz:joinModeType", + "@type" => "rdfs:Class" + }, + "joinMode" => %{ + "@id" => "mz:joinMode", + "@type" => "mz:joinModeType" + } } ] } @@ -112,128 +122,56 @@ defmodule Mobilizon.Service.ActivityPub.Utils do Map.put_new_lazy(map, "published", &make_date/0) end - @doc """ - Inserts a full object if it is contained in an activity. - """ - def insert_full_object(object_data) - - @doc """ - Inserts a full object if it is contained in an activity. - """ - def insert_full_object(%{"object" => %{"type" => "Event"} = object_data, "type" => "Create"}) - when is_map(object_data) do - with {:ok, object_data} <- - Converter.Event.as_to_model_data(object_data), - {:ok, %Event{} = event} <- Events.create_event(object_data) do - {:ok, event} - end + def get_actor(%{"actor" => actor}) when is_binary(actor) do + actor end - def insert_full_object(%{"object" => %{"type" => "Group"} = object_data, "type" => "Create"}) - when is_map(object_data) do - with object_data <- - Map.put(object_data, "preferred_username", object_data["preferredUsername"]), - {:ok, %Actor{} = group} <- Actors.create_group(object_data) do - {:ok, group} - end - end - - @doc """ - Inserts a full object if it is contained in an activity. - """ - def insert_full_object(%{"object" => %{"type" => "Note"} = object_data, "type" => "Create"}) - when is_map(object_data) do - with data <- Converter.Comment.as_to_model_data(object_data), - {:ok, %Comment{} = comment} <- Events.create_comment(data) do - {:ok, comment} + def get_actor(%{"actor" => actor}) when is_list(actor) do + if is_binary(Enum.at(actor, 0)) do + Enum.at(actor, 0) else - err -> - Logger.error("Error while inserting a remote comment inside database") - Logger.debug(inspect(err)) - {:error, err} + actor + |> Enum.find(fn %{"type" => type} -> type in ["Person", "Service", "Application"] end) + |> Map.get("id") end end + def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do + id + end + + def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do + get_actor(%{"actor" => actor}) + end + @doc """ - Inserts a full object if it is contained in an activity. + Checks that an incoming AP object's actor matches the domain it came from. """ - def insert_full_object(%{"type" => "Flag"} = object_data) - when is_map(object_data) do - with data <- Converter.Flag.as_to_model_data(object_data), - {:ok, %Report{} = report} <- Reports.create_report(data) do - Enum.each(Users.list_moderators(), fn moderator -> - moderator - |> Email.Admin.report(report) - |> Email.Mailer.deliver_later() - 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)) - {:ok, report} - else - err -> - Logger.error("Error while inserting report inside database") - Logger.debug(inspect(err)) - {:error, err} - end + compare_uris?(actor_uri, id_uri) end - def insert_full_object(_), do: {:ok, nil} + def origin_check?(_id, %{"actor" => nil}), do: false - @doc """ - Update an object - """ - @spec update_object(struct(), map()) :: {:ok, struct()} | any() - def update_object(object, object_data) + def origin_check?(id, %{"attributedTo" => actor} = params), + do: origin_check?(id, Map.put(params, "actor", actor)) - def update_object(event_url, %{ - "object" => %{"type" => "Event"} = object_data, - "type" => "Update" - }) - when is_map(object_data) do - with {:event_not_found, %Event{} = event} <- - {:event_not_found, Events.get_event_by_url(event_url)}, - {:ok, object_data} <- Converter.Event.as_to_model_data(object_data), - {:ok, %Event{} = event} <- Events.update_event(event, object_data) do - {:ok, event} - end + def origin_check?(_id, _data), do: false + + defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), do: id_uri.host == other_uri.host + + 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) + + compare_uris?(id_uri, other_uri) end - def update_object(actor_url, %{ - "object" => %{"type" => type_actor} = object_data, - "type" => "Update" - }) - when is_map(object_data) and type_actor in @actor_types do - with {:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor_url), - object_data <- Converter.Actor.as_to_model_data(object_data), - {:ok, %Actor{} = actor} <- Actors.update_actor(actor, object_data) do - {:ok, actor} - end - end - - def update_object(_, _), do: {:ok, nil} - - #### Like-related helpers - - # @doc """ - # Returns an existing like if a user already liked an object - # """ - # def get_existing_like(actor, %{data: %{"id" => id}}) do - # query = - # from( - # activity in Activity, - # where: fragment("(?)->>'actor' = ?", activity.data, ^actor), - # # this is to use the index - # where: - # fragment( - # "coalesce((?)->'object'->>'id', (?)->>'object') = ?", - # activity.data, - # activity.data, - # ^id - # ), - # where: fragment("(?)->>'type' = 'Like'", activity.data) - # ) - # - # Repo.one(query) - # end + def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id), + do: origin_check_from_id?(id, other_id) @doc """ Save picture data from %Plug.Upload{} and return AS Link data. @@ -284,255 +222,6 @@ defmodule Mobilizon.Service.ActivityPub.Utils do def make_picture_data(nil), do: nil - @doc """ - Make an AP event object from an set of values - """ - @spec make_event_data( - String.t(), - map(), - String.t(), - String.t(), - map(), - list(), - map(), - String.t() - ) :: map() - def make_event_data( - actor, - %{to: to, cc: cc} = _audience, - title, - content_html, - picture \\ nil, - tags \\ [], - metadata \\ %{}, - uuid \\ nil, - url \\ nil - ) do - Logger.debug("Making event data") - uuid = uuid || Ecto.UUID.generate() - - res = %{ - "type" => "Event", - "to" => to, - "cc" => cc || [], - "content" => content_html, - "name" => title, - "startTime" => metadata.begins_on, - "endTime" => metadata.ends_on, - "category" => metadata.category, - "actor" => actor, - "id" => url || Routes.page_url(Endpoint, :event, uuid), - "joinOptions" => metadata.join_options, - "status" => metadata.status, - "onlineAddress" => metadata.online_address, - "phoneAddress" => metadata.phone_address, - "draft" => metadata.draft, - "uuid" => uuid, - "tag" => - tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end) - } - - res = - if is_nil(metadata.physical_address), - do: res, - else: Map.put(res, "location", make_address_data(metadata.physical_address)) - - res = - if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)]) - - if is_nil(metadata.options) do - res - else - options = Events.EventOptions |> struct(metadata.options) |> Map.from_struct() - - Enum.reduce(options, res, fn {key, value}, acc -> - (!is_nil(value) && Map.put(acc, camelize(key), value)) || - acc - end) - end - end - - def make_address_data(%Address{} = address) do - # res = %{ - # "type" => "Place", - # "name" => address.description, - # "id" => address.url, - # "address" => %{ - # "type" => "PostalAddress", - # "streetAddress" => address.street, - # "postalCode" => address.postal_code, - # "addressLocality" => address.locality, - # "addressRegion" => address.region, - # "addressCountry" => address.country - # } - # } - # - # if is_nil(address.geom) do - # res - # else - # Map.put(res, "geo", %{ - # "type" => "GeoCoordinates", - # "latitude" => address.geom.coordinates |> elem(0), - # "longitude" => address.geom.coordinates |> elem(1) - # }) - # end - address.url - end - - def make_address_data(address) when is_map(address) do - Address - |> struct(address) - |> make_address_data() - end - - def make_address_data(address_url) when is_bitstring(address_url) do - with %Address{} = address <- Addresses.get_address_by_url(address_url) do - address.url - end - end - - @doc """ - Make an AP comment object from an set of values - """ - def make_comment_data( - actor, - to, - content_html, - # attachments, - inReplyTo \\ nil, - tags \\ [], - # _cw \\ nil, - cc \\ [] - ) do - Logger.debug("Making comment data") - uuid = Ecto.UUID.generate() - - object = %{ - "type" => "Note", - "to" => to, - "cc" => cc, - "content" => content_html, - # "summary" => cw, - # "attachment" => attachments, - "actor" => actor, - "id" => Routes.page_url(Endpoint, :comment, uuid), - "uuid" => uuid, - "tag" => tags |> Enum.uniq() - } - - if inReplyTo do - object - |> Map.put("inReplyTo", inReplyTo) - else - object - end - end - - def make_group_data( - actor, - to, - preferred_username, - content_html, - # attachments, - tags \\ [], - # _cw \\ nil, - cc \\ [] - ) do - uuid = Ecto.UUID.generate() - - %{ - "type" => "Group", - "to" => to, - "cc" => cc, - "summary" => content_html, - "attributedTo" => actor, - "preferredUsername" => preferred_username, - "id" => Actor.build_url(preferred_username, :page), - "uuid" => uuid, - "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() - } - end - - #### Like-related helpers - - @doc """ - Returns an existing like if a user already liked an object - """ - # @spec get_existing_like(Actor.t, map()) :: nil - # def get_existing_like(%Actor{url: url} = actor, %{data: %{"id" => id}}) do - # nil - # end - - # def make_like_data(%Actor{url: url} = actor, %{data: %{"id" => id}} = object, activity_id) do - # data = %{ - # "type" => "Like", - # "actor" => url, - # "object" => id, - # "to" => [actor.followers_url, object.data["actor"]], - # "cc" => ["https://www.w3.org/ns/activitystreams#Public"], - # "context" => object.data["context"] - # } - - # if activity_id, do: Map.put(data, "id", activity_id), else: data - # end - - def update_element_in_object(property, element, object) do - with new_data <- - object.data - |> Map.put("#{property}_count", length(element)) - |> Map.put("#{property}s", element), - changeset <- Changeset.change(object, data: new_data), - {:ok, object} <- Repo.update(changeset) do - {:ok, object} - end - end - - # def update_likes_in_object(likes, object) do - # update_element_in_object("like", likes, object) - # end - # - # def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do - # with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do - # update_likes_in_object(likes, object) - # end - # end - # - # def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do - # with likes <- (object.data["likes"] || []) |> List.delete(actor) do - # update_likes_in_object(likes, object) - # end - # end - - #### Follow-related helpers - - @doc """ - Makes a follow activity data for the given followed and follower - """ - def make_follow_data(%Actor{url: followed_id}, %Actor{url: follower_id}, activity_id) do - Logger.debug("Make follow data") - - data = %{ - "type" => "Follow", - "actor" => follower_id, - "to" => [followed_id], - "cc" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => followed_id - } - - data = - if activity_id, - do: Map.put(data, "id", activity_id), - else: data - - Logger.debug(inspect(data)) - - data - end - - #### Announce-related helpers - - require Logger - @doc """ Make announce activity data for the given actor and object """ @@ -673,42 +362,6 @@ defmodule Mobilizon.Service.ActivityPub.Utils do |> Map.merge(additional) end - #### Flag-related helpers - @spec make_flag_data(map(), map()) :: map() - def make_flag_data(params, additional) do - object = [params.reported_actor_url] ++ params.comments_url - - object = if params[:event_url], do: object ++ [params.event_url], else: object - - %{ - "type" => "Flag", - "id" => "#{MobilizonWeb.Endpoint.url()}/report/#{Ecto.UUID.generate()}", - "actor" => params.reporter_url, - "content" => params.content, - "object" => object, - "state" => "open" - } - |> Map.merge(additional) - end - - def make_join_data(%Event{} = event, %Actor{} = actor) do - %{ - "type" => "Join", - "id" => "#{actor.url}/join/event/id", - "actor" => actor.url, - "object" => event.url - } - end - - def make_join_data(%Actor{type: :Group} = event, %Actor{} = actor) do - %{ - "type" => "Join", - "id" => "#{actor.url}/join/group/id", - "actor" => actor.url, - "object" => event.url - } - end - @doc """ Make accept join activity data """ @@ -718,7 +371,6 @@ defmodule Mobilizon.Service.ActivityPub.Utils do "type" => "Accept", "to" => object["to"], "cc" => object["cc"], - "actor" => object["actor"], "object" => object, "id" => object["id"] <> "/activity" } @@ -741,37 +393,39 @@ defmodule Mobilizon.Service.ActivityPub.Utils do end end - @doc """ - Converts PEM encoded keys to a private key representation - """ - def pem_to_private_key(pem) do - [private_key_code] = :public_key.pem_decode(pem) - :public_key.pem_entry_decode(private_key_code) - end - - @doc """ - Converts PEM encoded keys to a PEM public key representation - """ def pem_to_public_key_pem(pem) do public_key = pem_to_public_key(pem) public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key) :public_key.pem_encode([public_key]) end - def camelize(word) when is_atom(word) do - camelize(to_string(word)) + defp make_signature(id, date) do + uri = URI.parse(id) + + signature = + Mobilizon.Service.ActivityPub.Relay.get_actor() + |> Mobilizon.Service.HTTPSignatures.Signature.sign(%{ + "(request-target)": "get #{uri.path}", + host: uri.host, + date: date + }) + + [{:Signature, signature}] end - def camelize(word) when is_bitstring(word) do - {first, rest} = String.split_at(Macro.camelize(word), 1) - String.downcase(first) <> rest + def sign_fetch(headers, id, date) do + if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do + headers ++ make_signature(id, date) + else + headers + end end - def underscore(word) when is_atom(word) do - underscore(to_string(word)) - end - - def underscore(word) when is_bitstring(word) do - Macro.underscore(word) + def maybe_date_fetch(headers, date) do + if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do + headers ++ [{:Date, date}] + else + headers + end end end diff --git a/lib/service/activity_pub/visibility.ex b/lib/service/activity_pub/visibility.ex index 018aacb63..fa89c6ec9 100644 --- a/lib/service/activity_pub/visibility.ex +++ b/lib/service/activity_pub/visibility.ex @@ -17,7 +17,10 @@ defmodule Mobilizon.Service.ActivityPub.Visibility do def is_public?(%{data: %{"type" => "Tombstone"}}), do: false def is_public?(%{data: data}), do: is_public?(data) def is_public?(%Activity{data: data}), do: is_public?(data) - def is_public?(data) when is_map(data), do: @public in (data["to"] ++ (data["cc"] || [])) + + def is_public?(data) when is_map(data), + do: @public in (Map.get(data, "to", []) ++ Map.get(data, "cc", [])) + def is_public?(%Comment{deleted_at: deleted_at}), do: !is_nil(deleted_at) def is_public?(err), do: raise(ArgumentError, message: "Invalid argument #{inspect(err)}") end diff --git a/lib/service/formatter/formatter.ex b/lib/service/formatter/formatter.ex index 282fc392f..33924d68c 100644 --- a/lib/service/formatter/formatter.ex +++ b/lib/service/formatter/formatter.ex @@ -34,16 +34,14 @@ defmodule Mobilizon.Service.Formatter do def mention_handler("@" <> nickname, buffer, _opts, acc) do case Actors.get_actor_by_name(nickname) do - %Actor{preferred_username: preferred_username} = actor -> - link = "@#{preferred_username}" + # %Actor{preferred_username: preferred_username} = actor -> + # link = "@#{preferred_username}" + # + # {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}} - {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}} - - %Actor{type: :Person, id: id, url: url, preferred_username: preferred_username} = actor -> + %Actor{type: :Person, id: id, preferred_username: preferred_username} = actor -> link = - "@#{ - preferred_username - }" + "@#{preferred_username}" {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}} diff --git a/lib/service/html.ex b/lib/service/html.ex index a30af219f..02c4d88c3 100644 --- a/lib/service/html.ex +++ b/lib/service/html.ex @@ -38,7 +38,8 @@ defmodule Mobilizon.Service.HTML.Scrubber.Default do "tag", "nofollow", "noopener", - "noreferrer" + "noreferrer", + "ugc" ]) Meta.allow_tag_with_these_attributes("a", ["name", "title"]) @@ -61,8 +62,8 @@ defmodule Mobilizon.Service.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes("ul", []) Meta.allow_tag_with_these_attributes("img", ["src", "alt"]) - Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"]) - Meta.allow_tag_with_these_attributes("span", []) + Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card", "mention"]) + Meta.allow_tag_with_these_attributes("span", ["data-user"]) Meta.allow_tag_with_these_attributes("h1", []) Meta.allow_tag_with_these_attributes("h2", []) diff --git a/lib/service/http_signatures/signature.ex b/lib/service/http_signatures/signature.ex index a4eb51ea2..df929617f 100644 --- a/lib/service/http_signatures/signature.ex +++ b/lib/service/http_signatures/signature.ex @@ -15,6 +15,7 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do require Logger + @spec key_id_to_actor_url(String.t()) :: String.t() def key_id_to_actor_url(key_id) do %{path: path} = uri = @@ -46,12 +47,10 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do end end - @doc """ - Gets a public key for a given ActivityPub actor ID (url). - """ + # Gets a public key for a given ActivityPub actor ID (url). @spec get_public_key_for_url(String.t()) :: {:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error} - def get_public_key_for_url(url) do + defp get_public_key_for_url(url) do with {:ok, %Actor{keys: keys}} <- ActivityPub.get_or_fetch_actor_by_url(url), {:ok, public_key} <- prepare_public_key(keys) do {:ok, public_key} @@ -103,16 +102,10 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do end end - def generate_date_header(date \\ Timex.now("GMT")) do - case Timex.format(date, "%a, %d %b %Y %H:%M:%S %Z", :strftime) do - {:ok, date} -> - date + def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now()) - {:error, err} -> - Logger.error("Unable to generate date header") - Logger.debug(inspect(err)) - nil - end + def generate_date_header(%NaiveDateTime{} = date) do + Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") end def generate_request_target(method, path), do: "#{method} #{path}" diff --git a/lib/service/workers/background_worker.ex b/lib/service/workers/background_worker.ex new file mode 100644 index 000000000..60cbe2122 --- /dev/null +++ b/lib/service/workers/background_worker.ex @@ -0,0 +1,17 @@ +defmodule Mobilizon.Service.Workers.BackgroundWorker do + @moduledoc """ + Worker to build search results + """ + + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor + + use Mobilizon.Service.Workers.WorkerHelper, queue: "background" + + @impl Oban.Worker + def perform(%{"op" => "delete_actor", "actor_id" => actor_id}, _job) do + with %Actor{} = actor <- Actors.get_actor(actor_id) do + Actors.perform(:delete_actor, actor) + end + end +end diff --git a/mix.exs b/mix.exs index d6612bb26..1591f74bb 100644 --- a/mix.exs +++ b/mix.exs @@ -60,6 +60,7 @@ defmodule Mobilizon.Mixfile do {:cowboy, "~> 2.6"}, {:guardian, "~> 2.0"}, {:guardian_db, "~> 2.0.2"}, + {:guardian_phoenix, "~> 2.0"}, {:argon2_elixir, "~> 2.0"}, {:cors_plug, "~> 2.0"}, {:ecto_autoslug_field, "~> 2.0"}, diff --git a/mix.lock b/mix.lock index 09d4aef61..650275303 100644 --- a/mix.lock +++ b/mix.lock @@ -60,6 +60,7 @@ "gettext": {:hex, :gettext, "0.17.1", "8baab33482df4907b3eae22f719da492cee3981a26e649b9c2be1c0192616962", [:mix], [], "hexpm"}, "guardian": {:hex, :guardian, "2.0.0", "5d3e537832b7cf35c8674da92457b7be671666a2eff4bf0f2ccfcfb3a8c67a0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "guardian_db": {:hex, :guardian_db, "2.0.2", "6247303fda5ed90e19ea1d2e4c5a65b13f58cc12810f95f71b6ffb50ef2d057f", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1.0", [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"}, + "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"}, "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [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]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "http_sign": {:hex, :http_sign, "0.1.1", "b16edb83aa282892f3271f9a048c155e772bf36e15700ab93901484c55f8dd10", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/mkdocs.yml b/mkdocs.yml index f72f58f39..1089c44d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,7 @@ markdown_extensions: - pymdownx.mark plugins: - search - - git-revision-date + - git-revision-date-localized theme: name: 'material' custom_dir: 'docs/theme/' diff --git a/priv/repo/migrations/20191129091227_add_timestamps_to_followers.exs b/priv/repo/migrations/20191129091227_add_timestamps_to_followers.exs new file mode 100644 index 000000000..dc582b82e --- /dev/null +++ b/priv/repo/migrations/20191129091227_add_timestamps_to_followers.exs @@ -0,0 +1,9 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddTimestampsToFollowers do + use Ecto.Migration + + def change do + alter table(:followers) do + timestamps() + end + end +end diff --git a/priv/repo/migrations/20191204164224_delete_event_cascade_to_comments.exs b/priv/repo/migrations/20191204164224_delete_event_cascade_to_comments.exs new file mode 100644 index 000000000..9d725dd81 --- /dev/null +++ b/priv/repo/migrations/20191204164224_delete_event_cascade_to_comments.exs @@ -0,0 +1,19 @@ +defmodule Mobilizon.Storage.Repo.Migrations.DeleteEventCascadeToComments do + use Ecto.Migration + + def up do + drop_if_exists(constraint(:comments, "comments_event_id_fkey")) + + alter table(:comments) do + modify(:event_id, references(:events, on_delete: :delete_all)) + end + end + + def down do + drop_if_exists(constraint(:comments, "comments_event_id_fkey")) + + alter table(:comments) do + modify(:event_id, references(:events, on_delete: :nothing)) + end + end +end diff --git a/priv/repo/migrations/20191206144028_create_shares.exs b/priv/repo/migrations/20191206144028_create_shares.exs new file mode 100644 index 000000000..ccfce195c --- /dev/null +++ b/priv/repo/migrations/20191206144028_create_shares.exs @@ -0,0 +1,23 @@ +defmodule Mobilizon.Repo.Migrations.CreateShares do + use Ecto.Migration + + def up do + create table(:shares) do + add(:uri, :string, null: false) + add(:actor_id, references(:actors, on_delete: :delete_all), null: false) + add(:owner_actor_id, references(:actors, on_delete: :delete_all), null: false) + + timestamps() + end + + create_if_not_exists( + index(:shares, [:uri, :actor_id], unique: true, name: :shares_uri_actor_id_index) + ) + end + + def down do + drop_if_exists(index(:shares, [:uri, :actor_id])) + + drop_if_exists(table(:shares)) + end +end diff --git a/schema.graphql b/schema.graphql index 66a412b0b..07dc7f638 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,9 +1,10 @@ # source: http://localhost:4000/api -# timestamp: Fri Nov 22 2019 18:34:33 GMT+0100 (Central European Standard Time) +# timestamp: Wed Dec 11 2019 15:24:29 GMT+0100 (heure normale d’Europe centrale) schema { query: RootQueryType mutation: RootMutationType + subscription: RootSubscriptionType } """An action log""" @@ -25,6 +26,7 @@ type ActionLog { } enum ActionLogAction { + COMMENT_DELETION EVENT_DELETION EVENT_UPDATE NOTE_CREATION @@ -66,9 +68,6 @@ interface Actor { """Internal ID for this actor""" id: ID - """The actors RSA Keys""" - keys: String - """If the actor is from this instance""" local: Boolean @@ -78,9 +77,6 @@ interface Actor { """The actor's displayed name""" name: String - """A list of the events this actor has organized""" - organizedEvents: [Event] - """The actor's preferred username""" preferredUsername: String @@ -155,8 +151,62 @@ input AddressInput { url: String } +""" +Represents an application + +""" +type Application implements Actor { + """The actor's avatar picture""" + avatar: Picture + + """The actor's banner picture""" + banner: Picture + + """The actor's domain if (null if it's this instance)""" + domain: String + + """List of followers""" + followers: [Follower] + + """Number of followers for this actor""" + followersCount: Int + + """List of followings""" + following: [Follower] + + """Number of actors following this actor""" + followingCount: Int + + """Internal ID for this application""" + id: ID + + """If the actor is from this instance""" + local: Boolean + + """Whether the actors manually approves followers""" + manuallyApprovesFollowers: Boolean + + """The actor's displayed name""" + name: String + + """The actor's preferred username""" + preferredUsername: String + + """The actor's summary""" + summary: String + + """If the actor is suspended""" + suspended: Boolean + + """The type of Actor (Person, Group,…)""" + type: ActorType + + """The ActivityPub actor's URL""" + url: String +} + """A comment""" -type Comment { +type Comment implements ActionLogObject { actor: Person deletedAt: DateTime event: Event @@ -542,8 +592,14 @@ type Follower { """Whether the follow has been approved by the target actor""" approved: Boolean + """When the follow was created""" + insertedAt: DateTime + """What or who the profile follows""" targetActor: Actor + + """When the follow was updated""" + updatedAt: DateTime } type Geocoding { @@ -580,9 +636,6 @@ type Group implements Actor { """Internal ID for this group""" id: ID - """The actors RSA Keys""" - keys: String - """If the actor is from this instance""" local: Boolean @@ -693,6 +746,14 @@ enum Openness { OPEN } +type PaginatedFollowerList { + """A list of followers""" + elements: [Follower] + + """The total number of elements in the list""" + total: Int +} + """Represents a participant to an event""" type Participant { """The actor that participates to the event""" @@ -772,9 +833,6 @@ type Person implements Actor { """Internal ID for this person""" id: ID - """The actors RSA Keys""" - keys: String - """If the actor is from this instance""" local: Boolean @@ -857,7 +915,7 @@ 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 @@ -941,11 +999,66 @@ type RootMutationType { """Change default actor for user""" changeDefaultActor(preferredUsername: String!): User - """Change an user password""" - changePassword(newPassword: String!, oldPassword: String!): 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 + """ + avatar: PictureInput - """Create a comment""" - createComment(actorId: ID!, eventId: ID, inReplyToCommentId: ID, text: String!): Comment + """ + The banner for the profile, either as an object or directly the ID of an existing Picture + """ + banner: PictureInput + + """The displayed name for the new profile""" + name: String = "" + preferredUsername: String! + + """The summary for the new profile""" + summary: String = "" + ): Person + + """Upload a picture""" + uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture + + """Delete an event""" + deleteEvent(actorId: ID!, eventId: ID!): DeletedObject + + """Create a note on a report""" + createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote + + """Accept a relay subscription""" + acceptRelay(address: String!): Follower + + """Delete a feed token""" + deleteFeedToken(token: String!): DeletedFeedToken + + """Validate an user after registration""" + validateUser(token: String!): Login + + """Resend registration confirmation token""" + resendConfirmationEmail(email: String!, locale: String): String + + """Update an identity""" + updatePerson( + """ + The avatar for the profile, either as an object or directly the ID of an existing Picture + """ + avatar: PictureInput + + """ + The banner for the profile, either as an object or directly the ID of an existing Picture + """ + banner: PictureInput + id: ID! + + """The displayed name for this profile""" + name: String + + """The summary for this profile""" + summary: String + ): Person """Create an event""" createEvent( @@ -974,95 +1087,6 @@ type RootMutationType { visibility: EventVisibility = PUBLIC ): Event - """Create a Feed Token""" - createFeedToken(actorId: ID): FeedToken - - """Create a group""" - createGroup( - """ - The avatar for the group, either as an object or directly the ID of an existing Picture - """ - avatar: PictureInput - - """ - The banner for the group, either as an object or directly the ID of an existing Picture - """ - banner: PictureInput - - """The identity that creates the group""" - creatorActorId: ID! - - """The displayed name for the group""" - name: String - - """The name for the group""" - preferredUsername: String! - - """The summary for the group""" - summary: String = "" - ): Group - - """Create a new person for user""" - createPerson( - """ - The avatar for the profile, either as an object or directly the ID of an existing Picture - """ - avatar: PictureInput - - """ - The banner for the profile, either as an object or directly the ID of an existing Picture - """ - banner: PictureInput - - """The displayed name for the new profile""" - name: String = "" - preferredUsername: String! - - """The summary for the new profile""" - summary: String = "" - ): Person - - """Create a report""" - createReport(commentsIds: [ID] = [""], content: String, eventId: ID, reportedActorId: ID!, reporterActorId: ID!): Report - - """Create a note on a report""" - createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote - - """Create an user""" - createUser(email: String!, locale: String, password: String!): User - deleteComment(actorId: ID!, commentId: ID!): DeletedObject - - """Delete an event""" - deleteEvent(actorId: ID!, eventId: ID!): DeletedObject - - """Delete a feed token""" - deleteFeedToken(token: String!): DeletedFeedToken - - """Delete a group""" - deleteGroup(actorId: ID!, groupId: ID!): DeletedObject - - """Delete an identity""" - deletePerson(id: ID!): Person - deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject - - """Join an event""" - joinEvent(actorId: ID!, eventId: ID!): Participant - - """Join a group""" - joinGroup(actorId: ID!, groupId: ID!): Member - - """Leave an event""" - leaveEvent(actorId: ID!, eventId: ID!): DeletedParticipant - - """Leave an event""" - leaveGroup(actorId: ID!, groupId: ID!): DeletedMember - - """Login an user""" - login(email: String!, password: String!): Login - - """Refresh a token""" - refreshToken(refreshToken: String!): RefreshedToken - """Register a first profile on registration""" registerPerson( """ @@ -1086,14 +1110,24 @@ type RootMutationType { summary: String = "" ): Person - """Resend registration confirmation token""" - resendConfirmationEmail(email: String!, locale: String): String + """Accept a participation""" + updateParticipation(id: ID!, moderatorActorId: ID!, role: ParticipantRoleEnum!): Participant - """Reset user password""" - resetPassword(locale: String = "en", password: String!, token: String!): Login + """Delete a group""" + deleteGroup(actorId: ID!, groupId: ID!): DeletedObject + deleteComment(actorId: ID!, commentId: ID!): Comment - """Send a link through email to reset user password""" - sendResetPassword(email: String!, locale: String): String + """Create an user""" + createUser(email: String!, locale: String, password: String!): User + + """Leave an event""" + leaveEvent(actorId: ID!, eventId: ID!): DeletedParticipant + + """Refresh a token""" + refreshToken(refreshToken: String!): RefreshedToken + + """Join a group""" + joinGroup(actorId: ID!, groupId: ID!): Member """Update an event""" updateEvent( @@ -1122,37 +1156,73 @@ type RootMutationType { visibility: EventVisibility = PUBLIC ): Event - """Accept a participation""" - updateParticipation(id: ID!, moderatorActorId: ID!, role: ParticipantRoleEnum!): Participant + """Reset user password""" + resetPassword(locale: String = "en", password: String!, token: String!): Login - """Update an identity""" - updatePerson( + """Create a report""" + createReport(commentsIds: [ID] = [""], content: String, eventId: ID, forward: Boolean = false, reportedId: ID!, reporterId: ID!): Report + + """Update a report""" + updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report + deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject + + """Delete a relay subscription""" + removeRelay(address: String!): Follower + + """Create a comment""" + createComment(actorId: ID!, eventId: ID, inReplyToCommentId: ID, text: String!): Comment + + """Delete an identity""" + deletePerson(id: ID!): Person + + """Reject a relay subscription""" + rejectRelay(address: String!): Follower + + """Login an user""" + login(email: String!, password: String!): Login + + """Leave an event""" + leaveGroup(actorId: ID!, groupId: ID!): DeletedMember + + """Change an user password""" + changePassword(newPassword: String!, oldPassword: String!): User + + """Add a relay subscription""" + addRelay(address: String!): Follower + + """Join an event""" + joinEvent(actorId: ID!, eventId: ID!): Participant + + """Create a group""" + createGroup( """ - The avatar for the profile, either as an object or directly the ID of an existing Picture + The avatar for the group, either as an object or directly the ID of an existing Picture """ avatar: PictureInput """ - The banner for the profile, either as an object or directly the ID of an existing Picture + The banner for the group, either as an object or directly the ID of an existing Picture """ banner: PictureInput - id: ID! - """The displayed name for this profile""" + """The identity that creates the group""" + creatorActorId: ID! + + """The displayed name for the group""" name: String - """The summary for this profile""" - summary: String - ): Person + """The name for the group""" + preferredUsername: String! - """Update a report""" - updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report + """The summary for the group""" + summary: String = "" + ): Group - """Upload a picture""" - uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture + """Send a link through email to reset user password""" + sendResetPassword(email: String!, locale: String): String - """Validate an user after registration""" - validateUser(token: String!): Login + """Create a Feed Token""" + createFeedToken(actorId: ID): FeedToken } """ @@ -1196,6 +1266,8 @@ type RootQueryType { """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 """Get a report by id""" report(id: ID!): Report @@ -1231,6 +1303,10 @@ type RootQueryType { 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""" enum SortableUserField { ID diff --git a/test/fixtures/mastodon-delete-user.json b/test/fixtures/mastodon-delete-user.json new file mode 100644 index 000000000..213354f02 --- /dev/null +++ b/test/fixtures/mastodon-delete-user.json @@ -0,0 +1,24 @@ +{ + "type": "Delete", + "object": { + "type": "Person", + "id": "https://framapiaf.org/users/admin", + "atomUri": "https://framapiaf.org/users/admin" + }, + "id": "https://framapiaf.org/users/admin#delete", + "actor": "https://framapiaf.org/users/admin", + "@context": [ + { + "toot": "http://joinmastodon.org/ns#", + "sensitive": "as:sensitive", + "ostatus": "http://ostatus.org#", + "movedTo": "as:movedTo", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "atomUri": "ostatus:atomUri", + "Hashtag": "as:Hashtag", + "Emoji": "toot:Emoji" + } + ] +} diff --git a/test/fixtures/mobilizon-join-activity.json b/test/fixtures/mobilizon-join-activity.json index f2669e3aa..8580ccf4a 100644 --- a/test/fixtures/mobilizon-join-activity.json +++ b/test/fixtures/mobilizon-join-activity.json @@ -11,12 +11,31 @@ "actor": "http://mobilizon2.test/@admin", "@context": [ "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", + "https://litepub.social/litepub/context.jsonld", { - "sensitive": "as:sensitive", - "movedTo": "as:movedTo", - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "Hashtag": "as:Hashtag" + "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" } ] } \ No newline at end of file diff --git a/test/fixtures/mobilizon-leave-activity.json b/test/fixtures/mobilizon-leave-activity.json index 10d157987..58e39ffcd 100644 --- a/test/fixtures/mobilizon-leave-activity.json +++ b/test/fixtures/mobilizon-leave-activity.json @@ -11,12 +11,31 @@ "actor": "http://mobilizon2.test/@admin", "@context": [ "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", + "https://litepub.social/litepub/context.jsonld", { - "sensitive": "as:sensitive", - "movedTo": "as:movedTo", - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "Hashtag": "as:Hashtag" + "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" } ] } \ No newline at end of file diff --git a/test/fixtures/mobilizon-post-activity.json b/test/fixtures/mobilizon-post-activity.json index 21a9ef442..1cba4dd04 100644 --- a/test/fixtures/mobilizon-post-activity.json +++ b/test/fixtures/mobilizon-post-activity.json @@ -1,13 +1,30 @@ { "@context": [ "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", + "https://litepub.social/litepub/context.jsonld", { - "mblzn": "https://joinmobilizon.org/ns#", "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#", - "Place": "sc:Place", - "PostalAddress": "sc:PostalAddress", "uuid": "sc:identifier" } ], diff --git a/test/fixtures/vcr_cassettes/activity_pub/activity_object_bogus.json b/test/fixtures/vcr_cassettes/activity_pub/activity_object_bogus.json new file mode 100644 index 000000000..f6d548a8f --- /dev/null +++ b/test/fixtures/vcr_cassettes/activity_pub/activity_object_bogus.json @@ -0,0 +1,116 @@ +[ + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "https://framapiaf.org/users/admin" + }, + "response": { + "binary": false, + "body": "{\"@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\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"}}],\"id\":\"https://framapiaf.org/users/admin\",\"type\":\"Service\",\"following\":\"https://framapiaf.org/users/admin/following\",\"followers\":\"https://framapiaf.org/users/admin/followers\",\"inbox\":\"https://framapiaf.org/users/admin/inbox\",\"outbox\":\"https://framapiaf.org/users/admin/outbox\",\"featured\":\"https://framapiaf.org/users/admin/collections/featured\",\"preferredUsername\":\"admin\",\"name\":\"Administrateur\",\"summary\":\"\\u003cp\\u003eJe ne suis qu\\u0026apos;un compte inutile. Merci nous de contacter via \\u003ca href=\\\"https://contact.framasoft.org/\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003econtact.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\",\"url\":\"https://framapiaf.org/@admin\",\"manuallyApprovesFollowers\":false,\"discoverable\":null,\"publicKey\":{\"id\":\"https://framapiaf.org/users/admin#main-key\",\"owner\":\"https://framapiaf.org/users/admin\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyHaU/AZ5dWtSxZXkPa89\\nDUQ4z+JQHGGUG/xkGuq0v8P6qJfQqtHPBO5vH0IQJqluXWQS96gqTwjZnYevcpNA\\nveYv0K25DWszx5Ehz6JX2/sSvu2rNUcQ3YZvSjdo/Yy1u5Fuc5lLmvw8uFzXYekD\\nWovTMOnp4mIKpVEm/G/v4w8jvFEKw88h743vwaEIim88GEQItMxzGAV6zSqV1DWO\\nLxtoRsinslJYfAG46ex4YUATFveWvOUeWk5W1sEa5f3c0moaTmBM/PAAo8vLxhlw\\nJhsHihsCH+BcXKVMjW8OCqYYqISMxEifUBX63HcJt78ELHpOuc1c2eG59PomtTjQ\\nywIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[],\"attachment\":[{\"type\":\"PropertyValue\",\"name\":\"News\",\"value\":\"\\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Support\",\"value\":\"\\u003ca href=\\\"https://contact.framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003econtact.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Soutenir\",\"value\":\"\\u003ca href=\\\"https://soutenir.framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003esoutenir.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Site\",\"value\":\"\\u003ca href=\\\"https://framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003eframasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"}],\"endpoints\":{\"sharedInbox\":\"https://framapiaf.org/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/000/002/original/85fbb27ad5e3cf71.jpg\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/headers/000/000/002/original/6aba75f1ab1ab6de.jpg\"}}", + "headers": { + "Date": "Sun, 15 Dec 2019 20:24:11 GMT", + "Content-Type": "application/activity+json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Server": "Mastodon", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "1; mode=block", + "Vary": "Accept, Accept-Encoding, Origin", + "Cache-Control": "max-age=180, public", + "ETag": "W/\"773e09a2a60446fe74d997858877f7e0\"", + "Set-Cookie": "_mastodon_session=XoZzbDOQtPbBNtbz7WoWZ4ZHTySKGQUeb1Hl3%2BxqZV%2FvxVEOMGMjKArqNF%2F78EJaF5TS%2FKcHPhwonEfsI5cz--jZG8CkvbBBmaJMDx--WRqeW2u0rVAHoNKcMNfLYA%3D%3D; path=/; secure; HttpOnly", + "X-Request-Id": "2a29bc1c-9dc8-4e36-b6d5-106c2f649959", + "X-Runtime": "0.012324", + "X-Cached": "MISS", + "Strict-Transport-Security": "max-age=31536000" + }, + "status_code": 200, + "type": "ok" + } + }, + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json", + "Date": "Sun, 15 Dec 2019 20:24:11 GMT", + "Signature": "keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) date host\",signature=\"C6AGJG5xDdDBYfoAgBaL0iG0/Ru9qpvUij5vvONxJJpcjwc4LIePLaCid4LWK4aiY/z67M+LgopmD7xRn2Qht+Cyu3Kh38t5dS8EI0RWR4JesRyBauCZpzbRJG2w5SES4BPt53+5AuvPZZ81BBgPYF4A7ITBn550NLocesuFFsJJZHwfNqRCUm4cmx57/tnLBr0S4w/VDn6iQ3TBSlXdUJ7N9Za9y7p+vfkFT2PqSXu55HdWLX5NvaiVl2m7JKBCxCrB3i4Lr/Og5bsKhi6LiUoc7Lp0LX1tNftp6NOGgBIo0982NQ0v2jGsMj+eGU2stDl6bz3Z9SRnyyxirz+fag==\"" + }, + "method": "get", + "options": { + "follow_redirect": "true", + "recv_timeout": 20000, + "connect_timeout": 10000 + }, + "request_body": "", + "url": "https://info.pleroma.site/activity.json" + }, + "response": { + "binary": false, + "body": "{\n \"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"actor\": \"https://queer.hacktivis.me/users/lanodan\",\n \"announcement_count\": 3,\n \"announcements\": [\n \"https://io.markegli.com/users/mark\",\n \"https://voluntaryism.club/users/sevvie\",\n \"https://pleroma.pla1.net/users/pla\"\n ],\n \"attachment\": [],\n \"attributedTo\": \"https://queer.hacktivis.me/users/lanodan\",\n \"content\": \"

this post was not actually written by Haelwenn

\",\n \"id\": \"https://info.pleroma.site/activity.json\",\n \"published\": \"2018-09-01T22:15:00Z\",\n \"tag\": [],\n \"to\": [\n \"https://www.w3.org/ns/activitystreams#Public\"\n ],\n \"type\": \"Note\"\n}\n", + "headers": { + "Server": "nginx", + "Date": "Sun, 15 Dec 2019 20:24:11 GMT", + "Content-Type": "application/json", + "Content-Length": "750", + "Last-Modified": "Sat, 01 Sep 2018 22:56:24 GMT", + "Connection": "keep-alive", + "ETag": "\"5b8b1918-2ee\"", + "Accept-Ranges": "bytes" + }, + "status_code": 200, + "type": "ok" + } + }, + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "https://queer.hacktivis.me/users/lanodan" + }, + "response": { + "binary": false, + "body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://queer.hacktivis.me/schemas/litepub-0.1.jsonld\",{\"@language\":\"und\"}],\"attachment\":[],\"discoverable\":false,\"endpoints\":{\"oauthAuthorizationEndpoint\":\"https://queer.hacktivis.me/oauth/authorize\",\"oauthRegistrationEndpoint\":\"https://queer.hacktivis.me/api/v1/apps\",\"oauthTokenEndpoint\":\"https://queer.hacktivis.me/oauth/token\",\"sharedInbox\":\"https://queer.hacktivis.me/inbox\",\"uploadMedia\":\"https://queer.hacktivis.me/api/ap/upload_media\"},\"followers\":\"https://queer.hacktivis.me/users/lanodan/followers\",\"following\":\"https://queer.hacktivis.me/users/lanodan/following\",\"icon\":{\"type\":\"Image\",\"url\":\"https://queer.hacktivis.me/media/c8e81887-7d81-4cdc-91b3-c624ea79e6c9/425f089961270eff91b66d45f8faeeb12a725a5f87a6a52bfc54c43bd89f5fe9.png\"},\"id\":\"https://queer.hacktivis.me/users/lanodan\",\"image\":{\"type\":\"Image\",\"url\":\"https://queer.hacktivis.me/media/37b6ce56-8c24-4e64-bd70-a76e84ab0c69/53a48a3a49ed5e5637a84e4f3663df17f8d764244bbc1027ba03cfc446e8b7bd.jpg\"},\"inbox\":\"https://queer.hacktivis.me/users/lanodan/inbox\",\"manuallyApprovesFollowers\":true,\"name\":\"Haelwenn /ɛlwən/ 🐺\",\"outbox\":\"https://queer.hacktivis.me/users/lanodan/outbox\",\"preferredUsername\":\"lanodan\",\"publicKey\":{\"id\":\"https://queer.hacktivis.me/users/lanodan#main-key\",\"owner\":\"https://queer.hacktivis.me/users/lanodan\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsWOgdjSMc010qvxC3njI\\nXJlFWMJ5gJ8QXCW/PajYdsHPM6d+jxBNJ6zp9/tIRa2m7bWHTSkuHQ7QthOpt6vu\\n+dAWpKRLS607SPLItn/qUcyXvgN+H8shfyhMxvkVs9jXdtlBsLUVE7UNpN0dxzqe\\nI79QWbf7o4amgaIWGRYB+OYMnIxKt+GzIkivZdSVSYjfxNnBYkMCeUxm5EpPIxKS\\nP5bBHAVRRambD5NUmyKILuC60/rYuc/C+vmgpY2HCWFS2q6o34dPr9enwL6t4b3m\\nS1t/EJHk9rGaaDqSGkDEfyQI83/7SDebWKuETMKKFLZi1vMgQIFuOYCIhN6bIiZm\\npQIDAQAB\\n-----END PUBLIC KEY-----\\n\\n\"},\"summary\":\"---
Website: https://hacktivis.me/
Pronouns: she/fae, elle/iel
Lang: en, fr, (LSF), ...
```
🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:
Pleroma dev (backend, mastofe)

banner from: https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db
Federation-bots: \",\"tag\":[{\"icon\":{\"type\":\"Image\",\"url\":\"https://queer.hacktivis.me/emoji/custom/symbols/anarchy.png\"},\"name\":\":anarchy:\",\"type\":\"Emoji\"},{\"icon\":{\"type\":\"Image\",\"url\":\"https://queer.hacktivis.me/emoji/custom/gentoo.png\"},\"name\":\":gentoo:\",\"type\":\"Emoji\"}],\"type\":\"Person\",\"url\":\"https://queer.hacktivis.me/users/lanodan\"}", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Sun, 15 Dec 2019 20:24:12 GMT", + "Content-Type": "application/activity+json; charset=utf-8", + "Content-Length": "2693", + "Connection": "keep-alive", + "Keep-Alive": "timeout=20", + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "access-control-expose-headers": "Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id,Idempotency-Key", + "cache-control": "max-age=0, private, must-revalidate", + "content-security-policy": "default-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; connect-src 'self' https://queer.hacktivis.me wss://queer.hacktivis.me; script-src 'self'; upgrade-insecure-requests;", + "expect-ct": "enforce, max-age=2592000", + "referrer-policy": "no-referrer", + "strict-transport-security": "max-age=31536000; includeSubDomains", + "x-content-type-options": "nosniff", + "x-download-options": "noopen", + "x-frame-options": "DENY", + "x-permitted-cross-domain-policies": "none", + "x-request-id": "FeClJfdJwAn7WY0ABtmh", + "x-xss-protection": "1; mode=block" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/activity_pub/event_update_activities.json b/test/fixtures/vcr_cassettes/activity_pub/event_update_activities.json index 20fbdb575..700e12e86 100644 --- a/test/fixtures/vcr_cassettes/activity_pub/event_update_activities.json +++ b/test/fixtures/vcr_cassettes/activity_pub/event_update_activities.json @@ -17,7 +17,7 @@ "body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://litepub.github.io/litepub/context.jsonld\",{\"Hashtag\":\"as:Hashtag\",\"category\":\"sc:category\",\"sc\":\"http://schema.org#\",\"uuid\":\"sc:identifier\"}],\"endpoints\":{\"sharedInbox\":\"https://test.mobilizon.org/inbox\"},\"followers\":\"https://test.mobilizon.org/@Alicia/followers\",\"following\":\"https://test.mobilizon.org/@Alicia/following\",\"id\":\"https://test.mobilizon.org/@Alicia\",\"inbox\":\"https://test.mobilizon.org/@Alicia/inbox\",\"manuallyApprovesFollowers\":false,\"name\":\"Alicia\",\"outbox\":\"https://test.mobilizon.org/@Alicia/outbox\",\"preferredUsername\":\"Alicia\",\"publicKey\":{\"id\":\"https://test.mobilizon.org/@Alicia#main-key\",\"owner\":\"https://test.mobilizon.org/@Alicia\",\"publicKeyPem\":\"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEAvb+emDoC6FCVpfo9Bh608sVsOK+8fun3UIqaR+jr+DZCAjp8ihwa\\nFkXaeOQ744MVS2YdzBEyIlk3sSYD9GezF+zoMbbA8FcnJ5jZhnneRR7ZrEg/cpNx\\nKFVA2ZoQrAABwpnA1iv7ciLoYZKPTDpIZ7Ue5l/k1bYcfTy0d4F3c8YAayWftSWj\\nHy3FK2kZDLdKfpRyfn5a4UI6sao4uD/rHno47g8tPPVA74BBpaTntJfbTWqiR8Vn\\nmNGAzy3+47pVeeg6Rd+AALohzBpHPW3TlJ75mqxPDXk7aDRYXihHrswf4MmKuaXc\\nXdoCu6uxQp41Xf3jVYD+AWw60tv2Oj/d4wIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"},\"summary\":\"J'aime le karaté, les mangas, coder en python.\",\"type\":\"Person\",\"url\":\"https://test.mobilizon.org/@Alicia\"}", "headers": { "Server": "nginx/1.14.2", - "Date": "Sun, 17 Nov 2019 18:12:35 GMT", + "Date": "Mon, 09 Dec 2019 17:24:25 GMT", "Content-Type": "application/activity+json; charset=utf-8", "Content-Length": "1293", "Connection": "keep-alive", @@ -25,12 +25,50 @@ "access-control-allow-origin": "*", "access-control-expose-headers": "", "cache-control": "max-age=0, private, must-revalidate", - "x-request-id": "FdgFt2eS9ln4-7YACVtC", + "x-request-id": "Fd7D2yizsbKxZroABLnC", "Strict-Transport-Security": "max-age=63072000; includeSubDomains", "X-Content-Type-Options": "nosniff" }, "status_code": 200, "type": "ok" } + }, + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "https://framapiaf.org/users/tcit" + }, + "response": { + "binary": false, + "body": "{\"@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\",\"Hashtag\":\"as:Hashtag\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"}}],\"id\":\"https://framapiaf.org/users/tcit\",\"type\":\"Person\",\"following\":\"https://framapiaf.org/users/tcit/following\",\"followers\":\"https://framapiaf.org/users/tcit/followers\",\"inbox\":\"https://framapiaf.org/users/tcit/inbox\",\"outbox\":\"https://framapiaf.org/users/tcit/outbox\",\"featured\":\"https://framapiaf.org/users/tcit/collections/featured\",\"preferredUsername\":\"tcit\",\"name\":\"💼 Thomas Citharel (Work)\",\"summary\":\"\\u003cp\\u003e\\u003ca href=\\\"https://framapiaf.org/tags/Framasoft\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/FreeSoftware\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eFreeSoftware\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Activism\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eActivism\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/wallabag\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003ewallabag\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Federation\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eFederation\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Nextcloud\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eNextcloud\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Mobilizon\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eMobilizon\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Libre\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eLibre\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\",\"url\":\"https://framapiaf.org/@tcit\",\"manuallyApprovesFollowers\":false,\"discoverable\":true,\"publicKey\":{\"id\":\"https://framapiaf.org/users/tcit#main-key\",\"owner\":\"https://framapiaf.org/users/tcit\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApscVCt06lrIiB5jT6Kqk\\nZZwPVoPkhR7HzoTGb8rnklZuOyP4goHIuBDnurklztkmDCaM7DbsUWAPgRVtwWFE\\nWuQrOenb7BPRe/m99pJfUTkBQU3IeuRMD/5Fc3OTIhHQOltTSiB900srCUxjysfw\\nnV5JFciCz8YAXTNJZD34qyv8DbtC/pCJM7wMd9Hl3ohxSPETa6CJUaTdlNwlYJa2\\nMOMCj6/7Iv5oAg14FT9lwqS5lF7jPHk9Z7PNc2wPmNVgIYA2n9d5k7JY8TdM8iu4\\nHLnIbJuqDd1uitlYgy1qsdsxjv4U2Y7Nytc+3ZKHtGsCzUltYL5kC7uWrFpGoWo1\\n0QIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/activism\",\"name\":\"#activism\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/federation\",\"name\":\"#federation\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/framasoft\",\"name\":\"#framasoft\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/freesoftware\",\"name\":\"#freesoftware\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/libre\",\"name\":\"#libre\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/mobilizon\",\"name\":\"#mobilizon\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/nextcloud\",\"name\":\"#nextcloud\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/wallabag\",\"name\":\"#wallabag\"}],\"attachment\":[{\"type\":\"PropertyValue\",\"name\":\"Personal account\",\"value\":\"\\u003ca href=\\\"https://social.tcit.fr/@tcit\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003esocial.tcit.fr/@tcit\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Location\",\"value\":\"Nantes, France\"},{\"type\":\"PropertyValue\",\"name\":\"Works at\",\"value\":\"\\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Website\",\"value\":\"\\u003ca href=\\\"https://tcit.fr\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003etcit.fr\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"IdentityProof\",\"name\":\"tcit\",\"signatureAlgorithm\":\"keybase\",\"signatureValue\":\"f66b45be42803010fe2f4d80e729b41bbe5ed056e2ff1286b7b5a5ea9c724cc70f\"}],\"endpoints\":{\"sharedInbox\":\"https://framapiaf.org/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/000/001/original/da0cad7ffd20eb61.jpg\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/headers/000/000/001/original/198d058b3086d82d.jpg\"}}", + "headers": { + "Date": "Mon, 09 Dec 2019 17:24:25 GMT", + "Content-Type": "application/activity+json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Server": "Mastodon", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "1; mode=block", + "Vary": "Accept, Accept-Encoding, Origin", + "Cache-Control": "max-age=180, public", + "ETag": "W/\"11665b12333a8c7708de7b17f58147b2\"", + "Set-Cookie": "_mastodon_session=l2BJyxnUWpNcZQ0u%2FaLJvf95IOy5b4PC1p1MVB7IGImBVhYbt6c0v6dcZcbtJzV%2FhPHF649GTTHeMixcyk1w--6lGoR%2F%2FAOMyZRQJi--fGHpQLKpoTLyRrgQQ7WW8g%3D%3D; path=/; secure; HttpOnly", + "X-Request-Id": "7d05233c-4a2d-4cf9-bcb6-6f405f6a370a", + "X-Runtime": "0.011783", + "X-Cached": "MISS", + "Strict-Transport-Security": "max-age=31536000" + }, + "status_code": 200, + "type": "ok" + } } ] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/activity_pub/fetch_mobilizon_post_activity.json b/test/fixtures/vcr_cassettes/activity_pub/fetch_mobilizon_post_activity.json index 051ac17b6..c099ca51f 100644 --- a/test/fixtures/vcr_cassettes/activity_pub/fetch_mobilizon_post_activity.json +++ b/test/fixtures/vcr_cassettes/activity_pub/fetch_mobilizon_post_activity.json @@ -17,7 +17,7 @@ "body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://litepub.github.io/litepub/context.jsonld\",{\"Hashtag\":\"as:Hashtag\",\"category\":\"sc:category\",\"sc\":\"http://schema.org#\",\"uuid\":\"sc:identifier\"}],\"endpoints\":{\"sharedInbox\":\"https://test.mobilizon.org/inbox\"},\"followers\":\"https://test.mobilizon.org/@Alicia/followers\",\"following\":\"https://test.mobilizon.org/@Alicia/following\",\"id\":\"https://test.mobilizon.org/@Alicia\",\"inbox\":\"https://test.mobilizon.org/@Alicia/inbox\",\"manuallyApprovesFollowers\":false,\"name\":\"Alicia\",\"outbox\":\"https://test.mobilizon.org/@Alicia/outbox\",\"preferredUsername\":\"Alicia\",\"publicKey\":{\"id\":\"https://test.mobilizon.org/@Alicia#main-key\",\"owner\":\"https://test.mobilizon.org/@Alicia\",\"publicKeyPem\":\"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEAvb+emDoC6FCVpfo9Bh608sVsOK+8fun3UIqaR+jr+DZCAjp8ihwa\\nFkXaeOQ744MVS2YdzBEyIlk3sSYD9GezF+zoMbbA8FcnJ5jZhnneRR7ZrEg/cpNx\\nKFVA2ZoQrAABwpnA1iv7ciLoYZKPTDpIZ7Ue5l/k1bYcfTy0d4F3c8YAayWftSWj\\nHy3FK2kZDLdKfpRyfn5a4UI6sao4uD/rHno47g8tPPVA74BBpaTntJfbTWqiR8Vn\\nmNGAzy3+47pVeeg6Rd+AALohzBpHPW3TlJ75mqxPDXk7aDRYXihHrswf4MmKuaXc\\nXdoCu6uxQp41Xf3jVYD+AWw60tv2Oj/d4wIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"},\"summary\":\"J'aime le karaté, les mangas, coder en python.\",\"type\":\"Person\",\"url\":\"https://test.mobilizon.org/@Alicia\"}", "headers": { "Server": "nginx/1.14.2", - "Date": "Sun, 17 Nov 2019 18:00:51 GMT", + "Date": "Mon, 09 Dec 2019 17:24:24 GMT", "Content-Type": "application/activity+json; charset=utf-8", "Content-Length": "1293", "Connection": "keep-alive", @@ -25,12 +25,50 @@ "access-control-allow-origin": "*", "access-control-expose-headers": "", "cache-control": "max-age=0, private, must-revalidate", - "x-request-id": "FdgFE5DoOmZXNz8ACVni", + "x-request-id": "Fd7D2v45MCtfaxgAB7bh", "Strict-Transport-Security": "max-age=63072000; includeSubDomains", "X-Content-Type-Options": "nosniff" }, "status_code": 200, "type": "ok" } + }, + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "https://framapiaf.org/users/tcit" + }, + "response": { + "binary": false, + "body": "{\"@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\",\"Hashtag\":\"as:Hashtag\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"}}],\"id\":\"https://framapiaf.org/users/tcit\",\"type\":\"Person\",\"following\":\"https://framapiaf.org/users/tcit/following\",\"followers\":\"https://framapiaf.org/users/tcit/followers\",\"inbox\":\"https://framapiaf.org/users/tcit/inbox\",\"outbox\":\"https://framapiaf.org/users/tcit/outbox\",\"featured\":\"https://framapiaf.org/users/tcit/collections/featured\",\"preferredUsername\":\"tcit\",\"name\":\"💼 Thomas Citharel (Work)\",\"summary\":\"\\u003cp\\u003e\\u003ca href=\\\"https://framapiaf.org/tags/Framasoft\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/FreeSoftware\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eFreeSoftware\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Activism\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eActivism\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/wallabag\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003ewallabag\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Federation\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eFederation\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Nextcloud\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eNextcloud\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Mobilizon\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eMobilizon\\u003c/span\\u003e\\u003c/a\\u003e \\u003ca href=\\\"https://framapiaf.org/tags/Libre\\\" class=\\\"mention hashtag\\\" rel=\\\"tag\\\"\\u003e#\\u003cspan\\u003eLibre\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\",\"url\":\"https://framapiaf.org/@tcit\",\"manuallyApprovesFollowers\":false,\"discoverable\":true,\"publicKey\":{\"id\":\"https://framapiaf.org/users/tcit#main-key\",\"owner\":\"https://framapiaf.org/users/tcit\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApscVCt06lrIiB5jT6Kqk\\nZZwPVoPkhR7HzoTGb8rnklZuOyP4goHIuBDnurklztkmDCaM7DbsUWAPgRVtwWFE\\nWuQrOenb7BPRe/m99pJfUTkBQU3IeuRMD/5Fc3OTIhHQOltTSiB900srCUxjysfw\\nnV5JFciCz8YAXTNJZD34qyv8DbtC/pCJM7wMd9Hl3ohxSPETa6CJUaTdlNwlYJa2\\nMOMCj6/7Iv5oAg14FT9lwqS5lF7jPHk9Z7PNc2wPmNVgIYA2n9d5k7JY8TdM8iu4\\nHLnIbJuqDd1uitlYgy1qsdsxjv4U2Y7Nytc+3ZKHtGsCzUltYL5kC7uWrFpGoWo1\\n0QIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/activism\",\"name\":\"#activism\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/federation\",\"name\":\"#federation\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/framasoft\",\"name\":\"#framasoft\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/freesoftware\",\"name\":\"#freesoftware\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/libre\",\"name\":\"#libre\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/mobilizon\",\"name\":\"#mobilizon\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/nextcloud\",\"name\":\"#nextcloud\"},{\"type\":\"Hashtag\",\"href\":\"https://framapiaf.org/explore/wallabag\",\"name\":\"#wallabag\"}],\"attachment\":[{\"type\":\"PropertyValue\",\"name\":\"Personal account\",\"value\":\"\\u003ca href=\\\"https://social.tcit.fr/@tcit\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003esocial.tcit.fr/@tcit\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Location\",\"value\":\"Nantes, France\"},{\"type\":\"PropertyValue\",\"name\":\"Works at\",\"value\":\"\\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Website\",\"value\":\"\\u003ca href=\\\"https://tcit.fr\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003etcit.fr\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"IdentityProof\",\"name\":\"tcit\",\"signatureAlgorithm\":\"keybase\",\"signatureValue\":\"f66b45be42803010fe2f4d80e729b41bbe5ed056e2ff1286b7b5a5ea9c724cc70f\"}],\"endpoints\":{\"sharedInbox\":\"https://framapiaf.org/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/000/001/original/da0cad7ffd20eb61.jpg\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/headers/000/000/001/original/198d058b3086d82d.jpg\"}}", + "headers": { + "Date": "Mon, 09 Dec 2019 17:24:25 GMT", + "Content-Type": "application/activity+json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Server": "Mastodon", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "1; mode=block", + "Vary": "Accept, Accept-Encoding, Origin", + "Cache-Control": "max-age=180, public", + "ETag": "W/\"11665b12333a8c7708de7b17f58147b2\"", + "Set-Cookie": "_mastodon_session=hkR%2BepdH7Hnl0wgxjtkiOa6%2FY8%2FIs4lElyGl%2FRMoRqdztBigOMH19196k1gDXNqqotlhjZMGBcDPv5tSOTdN--UzxEtxF4SS5Vwfhn--S3FqvLDMaBYDpE2P4o64Nw%3D%3D; path=/; secure; HttpOnly", + "X-Request-Id": "88981ba1-9aa8-428c-a868-78a918cf1317", + "X-Runtime": "0.013394", + "X-Cached": "MISS", + "Strict-Transport-Security": "max-age=31536000" + }, + "status_code": 200, + "type": "ok" + } } ] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/activity_pub/object_bogus_origin.json b/test/fixtures/vcr_cassettes/activity_pub/object_bogus_origin.json new file mode 100644 index 000000000..88f08bbb7 --- /dev/null +++ b/test/fixtures/vcr_cassettes/activity_pub/object_bogus_origin.json @@ -0,0 +1,78 @@ +[ + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json", + "Date": "Sun, 15 Dec 2019 20:24:06 GMT", + "Signature": "keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) date host\",signature=\"kUYhmOSdg7kxApgTCEj1CZLwOGn31sLbPdU35xu6Y5f/I3oT65g6KYlzuvchfQTrJ0XTzdzdSpQ5ZC/Y5dQEZrc3yzGw0KOmpSFfDcbtLw0I4Ya3vRjvuw0cDsPUwxoKpW1FqkMIEXP2lTXV/Bywc2rWCytnttiJwoQdeTvPDigmCCLxo2+wNMshl169HjAjYT9T8O0ptlgZXZ+JPuuaMj6EcSnXJDAkDxdBo54D61ED+dIIDxRKJsCTDrnjvZ86E9Z/P8SIbQxPZNw+TpLeofFTi/xt0E42M76iOEk41+kKlWQBd4imwoJesYEU+7CsMRtttt7wK9hMqcQC4UCa8g==\"" + }, + "method": "get", + "options": { + "follow_redirect": "true", + "recv_timeout": 20000, + "connect_timeout": 10000 + }, + "request_body": "", + "url": "https://info.pleroma.site/activity.json" + }, + "response": { + "binary": false, + "body": "{\n \"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"actor\": \"https://queer.hacktivis.me/users/lanodan\",\n \"announcement_count\": 3,\n \"announcements\": [\n \"https://io.markegli.com/users/mark\",\n \"https://voluntaryism.club/users/sevvie\",\n \"https://pleroma.pla1.net/users/pla\"\n ],\n \"attachment\": [],\n \"attributedTo\": \"https://queer.hacktivis.me/users/lanodan\",\n \"content\": \"

this post was not actually written by Haelwenn

\",\n \"id\": \"https://info.pleroma.site/activity.json\",\n \"published\": \"2018-09-01T22:15:00Z\",\n \"tag\": [],\n \"to\": [\n \"https://www.w3.org/ns/activitystreams#Public\"\n ],\n \"type\": \"Note\"\n}\n", + "headers": { + "Server": "nginx", + "Date": "Sun, 15 Dec 2019 20:24:07 GMT", + "Content-Type": "application/json", + "Content-Length": "750", + "Last-Modified": "Sat, 01 Sep 2018 22:56:24 GMT", + "Connection": "keep-alive", + "ETag": "\"5b8b1918-2ee\"", + "Accept-Ranges": "bytes" + }, + "status_code": 200, + "type": "ok" + } + }, + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "https://queer.hacktivis.me/users/lanodan" + }, + "response": { + "binary": false, + "body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://queer.hacktivis.me/schemas/litepub-0.1.jsonld\",{\"@language\":\"und\"}],\"attachment\":[],\"discoverable\":false,\"endpoints\":{\"oauthAuthorizationEndpoint\":\"https://queer.hacktivis.me/oauth/authorize\",\"oauthRegistrationEndpoint\":\"https://queer.hacktivis.me/api/v1/apps\",\"oauthTokenEndpoint\":\"https://queer.hacktivis.me/oauth/token\",\"sharedInbox\":\"https://queer.hacktivis.me/inbox\",\"uploadMedia\":\"https://queer.hacktivis.me/api/ap/upload_media\"},\"followers\":\"https://queer.hacktivis.me/users/lanodan/followers\",\"following\":\"https://queer.hacktivis.me/users/lanodan/following\",\"icon\":{\"type\":\"Image\",\"url\":\"https://queer.hacktivis.me/media/c8e81887-7d81-4cdc-91b3-c624ea79e6c9/425f089961270eff91b66d45f8faeeb12a725a5f87a6a52bfc54c43bd89f5fe9.png\"},\"id\":\"https://queer.hacktivis.me/users/lanodan\",\"image\":{\"type\":\"Image\",\"url\":\"https://queer.hacktivis.me/media/37b6ce56-8c24-4e64-bd70-a76e84ab0c69/53a48a3a49ed5e5637a84e4f3663df17f8d764244bbc1027ba03cfc446e8b7bd.jpg\"},\"inbox\":\"https://queer.hacktivis.me/users/lanodan/inbox\",\"manuallyApprovesFollowers\":true,\"name\":\"Haelwenn /ɛlwən/ 🐺\",\"outbox\":\"https://queer.hacktivis.me/users/lanodan/outbox\",\"preferredUsername\":\"lanodan\",\"publicKey\":{\"id\":\"https://queer.hacktivis.me/users/lanodan#main-key\",\"owner\":\"https://queer.hacktivis.me/users/lanodan\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsWOgdjSMc010qvxC3njI\\nXJlFWMJ5gJ8QXCW/PajYdsHPM6d+jxBNJ6zp9/tIRa2m7bWHTSkuHQ7QthOpt6vu\\n+dAWpKRLS607SPLItn/qUcyXvgN+H8shfyhMxvkVs9jXdtlBsLUVE7UNpN0dxzqe\\nI79QWbf7o4amgaIWGRYB+OYMnIxKt+GzIkivZdSVSYjfxNnBYkMCeUxm5EpPIxKS\\nP5bBHAVRRambD5NUmyKILuC60/rYuc/C+vmgpY2HCWFS2q6o34dPr9enwL6t4b3m\\nS1t/EJHk9rGaaDqSGkDEfyQI83/7SDebWKuETMKKFLZi1vMgQIFuOYCIhN6bIiZm\\npQIDAQAB\\n-----END PUBLIC KEY-----\\n\\n\"},\"summary\":\"---
Website: https://hacktivis.me/
Pronouns: she/fae, elle/iel
Lang: en, fr, (LSF), ...
```
🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:
Pleroma dev (backend, mastofe)

banner from: https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db
Federation-bots: \",\"tag\":[{\"icon\":{\"type\":\"Image\",\"url\":\"https://queer.hacktivis.me/emoji/custom/symbols/anarchy.png\"},\"name\":\":anarchy:\",\"type\":\"Emoji\"},{\"icon\":{\"type\":\"Image\",\"url\":\"https://queer.hacktivis.me/emoji/custom/gentoo.png\"},\"name\":\":gentoo:\",\"type\":\"Emoji\"}],\"type\":\"Person\",\"url\":\"https://queer.hacktivis.me/users/lanodan\"}", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Sun, 15 Dec 2019 20:24:07 GMT", + "Content-Type": "application/activity+json; charset=utf-8", + "Content-Length": "2693", + "Connection": "keep-alive", + "Keep-Alive": "timeout=20", + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "access-control-expose-headers": "Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id,Idempotency-Key", + "cache-control": "max-age=0, private, must-revalidate", + "content-security-policy": "default-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; connect-src 'self' https://queer.hacktivis.me wss://queer.hacktivis.me; script-src 'self'; upgrade-insecure-requests;", + "expect-ct": "enforce, max-age=2592000", + "referrer-policy": "no-referrer", + "strict-transport-security": "max-age=31536000; includeSubDomains", + "x-content-type-options": "nosniff", + "x-download-options": "noopen", + "x-frame-options": "DENY", + "x-permitted-cross-domain-policies": "none", + "x-request-id": "FeClJPRE1U5AcP8ABtkB", + "x-xss-protection": "1; mode=block" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/activity_pub/signature/invalid_not_found.json b/test/fixtures/vcr_cassettes/activity_pub/signature/invalid_not_found.json new file mode 100644 index 000000000..194a42c68 --- /dev/null +++ b/test/fixtures/vcr_cassettes/activity_pub/signature/invalid_not_found.json @@ -0,0 +1,39 @@ +[ + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "http://niu.moe/users/rye" + }, + "response": { + "binary": false, + "body": "{\"@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\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"}}],\"id\":\"https://niu.moe/users/rye\",\"type\":\"Person\",\"following\":\"https://niu.moe/users/rye/following\",\"followers\":\"https://niu.moe/users/rye/followers\",\"inbox\":\"https://niu.moe/users/rye/inbox\",\"outbox\":\"https://niu.moe/users/rye/outbox\",\"featured\":\"https://niu.moe/users/rye/collections/featured\",\"preferredUsername\":\"rye\",\"name\":\"♡ rye ♡\",\"summary\":\"\\u003cp\\u003eicon from \\u003ca href=\\\"https://twitter.com/_nitronic/status/1137776178687725568\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"ellipsis\\\"\\u003etwitter.com/_nitronic/status/1\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e137776178687725568\\u003c/span\\u003e\\u003c/a\\u003e くコ:彡\\u003c/p\\u003e\\u003cp\\u003eCome back with a warrant\\u003c/p\\u003e\",\"url\":\"https://niu.moe/@rye\",\"manuallyApprovesFollowers\":false,\"discoverable\":false,\"publicKey\":{\"id\":\"https://niu.moe/users/rye#main-key\",\"owner\":\"https://niu.moe/users/rye\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA83uRWjCFO35FwfA38mzv\\nEL0TUaXB7+2hYvPwNrn1WY6me5DRbqB5zzMrzWMGr0HSooqNqEYBafGsmVTWUqIk\\nKM9ehtIBraJI+mT5X7DPR3LrXOJF4a9EEslg8XvAk8MN9IrAhm6UljnvB67RtDcA\\nTNB01VWy9yWnxFRtz9o/EMoBPyw5giOaXE2ibVNP8lQIqGKuuBKPzPjSJygdvQ5q\\nxfow2z1TpKRqdsNDqn4n6U6zCXYTzkr0J71/tGw7fsgfv78l0Wjrc7EcuBk74OaG\\nC65UDiu3X4Q6kxCfCEhPSfuwLN+UZkzxcn6goWR0iYpWs57+4tFKu9nJYP4QJ0K9\\nTwIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[],\"attachment\":[],\"endpoints\":{\"sharedInbox\":\"https://niu.moe/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/png\",\"url\":\"https://cdn.niu.moe/accounts/avatars/000/033/323/original/e4d637b2c8755a7e.png\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://cdn.niu.moe/accounts/headers/000/033/323/original/cc89e1bc66b99a65.jpeg\"}}", + "headers": { + "Cache-Control": "max-age=180, public", + "Content-Type": "application/activity+json; charset=utf-8", + "Date": "Fri, 06 Dec 2019 10:26:01 GMT", + "Etag": "W/\"b3c2a8220a20671e8ca5c7e3371e7f5a\"", + "Server": "Caddy", + "Set-Cookie": "_mastodon_session=lSwFzD6GF6%2FjEL7hWLHU61n7%2B8kC60xvZYlPZG8EBtcndPzfd2%2B976zDOf1ALGZkvqj3CdpYHbZyq%2B7cwfkX--Ut%2BKGA8YibOTCEhb--a0sE5cHGI5PicAmO2yDlZw%3D%3D; path=/; secure; HttpOnly", + "Strict-Transport-Security": "max-age=31536000", + "Vary": "Accept, Accept-Encoding, Origin", + "X-Cached": "MISS", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "X-Request-Id": "0cc18bab-2d72-47b2-8778-8646402e1148", + "X-Runtime": "0.009213", + "X-Xss-Protection": "1; mode=block", + "Transfer-Encoding": "chunked" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/activity_pub/signature/invalid_payload.json b/test/fixtures/vcr_cassettes/activity_pub/signature/invalid_payload.json new file mode 100644 index 000000000..f150c2360 --- /dev/null +++ b/test/fixtures/vcr_cassettes/activity_pub/signature/invalid_payload.json @@ -0,0 +1,39 @@ +[ + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "https://niu.moe/users/rye" + }, + "response": { + "binary": false, + "body": "{\"@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\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"}}],\"id\":\"https://niu.moe/users/rye\",\"type\":\"Person\",\"following\":\"https://niu.moe/users/rye/following\",\"followers\":\"https://niu.moe/users/rye/followers\",\"inbox\":\"https://niu.moe/users/rye/inbox\",\"outbox\":\"https://niu.moe/users/rye/outbox\",\"featured\":\"https://niu.moe/users/rye/collections/featured\",\"preferredUsername\":\"rye\",\"name\":\"♡ rye ♡\",\"summary\":\"\\u003cp\\u003eicon from \\u003ca href=\\\"https://twitter.com/_nitronic/status/1137776178687725568\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"ellipsis\\\"\\u003etwitter.com/_nitronic/status/1\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e137776178687725568\\u003c/span\\u003e\\u003c/a\\u003e くコ:彡\\u003c/p\\u003e\\u003cp\\u003eCome back with a warrant\\u003c/p\\u003e\",\"url\":\"https://niu.moe/@rye\",\"manuallyApprovesFollowers\":false,\"discoverable\":false,\"publicKey\":{\"id\":\"https://niu.moe/users/rye#main-key\",\"owner\":\"https://niu.moe/users/rye\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA83uRWjCFO35FwfA38mzv\\nEL0TUaXB7+2hYvPwNrn1WY6me5DRbqB5zzMrzWMGr0HSooqNqEYBafGsmVTWUqIk\\nKM9ehtIBraJI+mT5X7DPR3LrXOJF4a9EEslg8XvAk8MN9IrAhm6UljnvB67RtDcA\\nTNB01VWy9yWnxFRtz9o/EMoBPyw5giOaXE2ibVNP8lQIqGKuuBKPzPjSJygdvQ5q\\nxfow2z1TpKRqdsNDqn4n6U6zCXYTzkr0J71/tGw7fsgfv78l0Wjrc7EcuBk74OaG\\nC65UDiu3X4Q6kxCfCEhPSfuwLN+UZkzxcn6goWR0iYpWs57+4tFKu9nJYP4QJ0K9\\nTwIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[],\"attachment\":[],\"endpoints\":{\"sharedInbox\":\"https://niu.moe/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/png\",\"url\":\"https://cdn.niu.moe/accounts/avatars/000/033/323/original/e4d637b2c8755a7e.png\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://cdn.niu.moe/accounts/headers/000/033/323/original/cc89e1bc66b99a65.jpeg\"}}", + "headers": { + "Cache-Control": "max-age=180, public", + "Content-Type": "application/activity+json; charset=utf-8", + "Date": "Fri, 06 Dec 2019 10:26:00 GMT", + "Etag": "W/\"06011deced02514fa9adc61bf61ed2fd\"", + "Server": "Caddy", + "Set-Cookie": "_mastodon_session=n67ChnKe59aqgHL9yq2ReOf2DXDK7c54n49moftpN5s3c6AJpyJ9QZUH31wz0eyDiSiHHw4A6IgzpkrhvSF0--Gm%2FvZr27eWvDBh23--BROo3uKkLhfRDQgszEDO3w%3D%3D; path=/; secure; HttpOnly", + "Strict-Transport-Security": "max-age=31536000", + "Vary": "Accept, Accept-Encoding, Origin", + "X-Cached": "MISS", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "X-Request-Id": "acee93a9-4da7-4495-bf03-ae7113c50b43", + "X-Runtime": "0.016198", + "X-Xss-Protection": "1; mode=block", + "Transfer-Encoding": "chunked" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/activity_pub/signature/valid.json b/test/fixtures/vcr_cassettes/activity_pub/signature/valid.json new file mode 100644 index 000000000..702cccddf --- /dev/null +++ b/test/fixtures/vcr_cassettes/activity_pub/signature/valid.json @@ -0,0 +1,40 @@ +[ + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "https://framapiaf.org/users/admin" + }, + "response": { + "binary": false, + "body": "{\"@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\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"}}],\"id\":\"https://framapiaf.org/users/admin\",\"type\":\"Service\",\"following\":\"https://framapiaf.org/users/admin/following\",\"followers\":\"https://framapiaf.org/users/admin/followers\",\"inbox\":\"https://framapiaf.org/users/admin/inbox\",\"outbox\":\"https://framapiaf.org/users/admin/outbox\",\"featured\":\"https://framapiaf.org/users/admin/collections/featured\",\"preferredUsername\":\"admin\",\"name\":\"Administrateur\",\"summary\":\"\\u003cp\\u003eJe ne suis qu\\u0026apos;un compte inutile. Merci nous de contacter via \\u003ca href=\\\"https://contact.framasoft.org/\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003econtact.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\",\"url\":\"https://framapiaf.org/@admin\",\"manuallyApprovesFollowers\":false,\"discoverable\":null,\"publicKey\":{\"id\":\"https://framapiaf.org/users/admin#main-key\",\"owner\":\"https://framapiaf.org/users/admin\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyHaU/AZ5dWtSxZXkPa89\\nDUQ4z+JQHGGUG/xkGuq0v8P6qJfQqtHPBO5vH0IQJqluXWQS96gqTwjZnYevcpNA\\nveYv0K25DWszx5Ehz6JX2/sSvu2rNUcQ3YZvSjdo/Yy1u5Fuc5lLmvw8uFzXYekD\\nWovTMOnp4mIKpVEm/G/v4w8jvFEKw88h743vwaEIim88GEQItMxzGAV6zSqV1DWO\\nLxtoRsinslJYfAG46ex4YUATFveWvOUeWk5W1sEa5f3c0moaTmBM/PAAo8vLxhlw\\nJhsHihsCH+BcXKVMjW8OCqYYqISMxEifUBX63HcJt78ELHpOuc1c2eG59PomtTjQ\\nywIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[],\"attachment\":[{\"type\":\"PropertyValue\",\"name\":\"News\",\"value\":\"\\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Support\",\"value\":\"\\u003ca href=\\\"https://contact.framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003econtact.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Soutenir\",\"value\":\"\\u003ca href=\\\"https://soutenir.framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003esoutenir.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Site\",\"value\":\"\\u003ca href=\\\"https://framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003eframasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"}],\"endpoints\":{\"sharedInbox\":\"https://framapiaf.org/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/000/002/original/85fbb27ad5e3cf71.jpg\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/headers/000/000/002/original/6aba75f1ab1ab6de.jpg\"}}", + "headers": { + "Date": "Fri, 06 Dec 2019 10:25:59 GMT", + "Content-Type": "application/activity+json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Server": "Mastodon", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "1; mode=block", + "Vary": "Accept, Accept-Encoding, Origin", + "Cache-Control": "max-age=180, public", + "ETag": "W/\"773e09a2a60446fe74d997858877f7e0\"", + "Set-Cookie": "_mastodon_session=2qQRsid4lIe3JZJLG3ZxxfaDP4XDPsKEtqr9Bf3tljCQUEYrQZtQ44k74K1S1VeO3d2O2ztK7eafBlOx0KGQ--FK4A%2Bp1X4tvqnhba--KDpnaRQfBtiHOWhqW7ECEg%3D%3D; path=/; secure; HttpOnly", + "X-Request-Id": "c70d5224-5f9a-481a-a6d0-b817b0054be9", + "X-Runtime": "0.004654", + "X-Cached": "MISS", + "Strict-Transport-Security": "max-age=31536000" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/activity_pub/signature/valid_payload.json b/test/fixtures/vcr_cassettes/activity_pub/signature/valid_payload.json new file mode 100644 index 000000000..6bbc7ffa2 --- /dev/null +++ b/test/fixtures/vcr_cassettes/activity_pub/signature/valid_payload.json @@ -0,0 +1,40 @@ +[ + { + "request": { + "body": "", + "headers": { + "Accept": "application/activity+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "https://framapiaf.org/users/admin" + }, + "response": { + "binary": false, + "body": "{\"@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\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"}}],\"id\":\"https://framapiaf.org/users/admin\",\"type\":\"Service\",\"following\":\"https://framapiaf.org/users/admin/following\",\"followers\":\"https://framapiaf.org/users/admin/followers\",\"inbox\":\"https://framapiaf.org/users/admin/inbox\",\"outbox\":\"https://framapiaf.org/users/admin/outbox\",\"featured\":\"https://framapiaf.org/users/admin/collections/featured\",\"preferredUsername\":\"admin\",\"name\":\"Administrateur\",\"summary\":\"\\u003cp\\u003eJe ne suis qu\\u0026apos;un compte inutile. Merci nous de contacter via \\u003ca href=\\\"https://contact.framasoft.org/\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003econtact.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\",\"url\":\"https://framapiaf.org/@admin\",\"manuallyApprovesFollowers\":false,\"discoverable\":null,\"publicKey\":{\"id\":\"https://framapiaf.org/users/admin#main-key\",\"owner\":\"https://framapiaf.org/users/admin\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyHaU/AZ5dWtSxZXkPa89\\nDUQ4z+JQHGGUG/xkGuq0v8P6qJfQqtHPBO5vH0IQJqluXWQS96gqTwjZnYevcpNA\\nveYv0K25DWszx5Ehz6JX2/sSvu2rNUcQ3YZvSjdo/Yy1u5Fuc5lLmvw8uFzXYekD\\nWovTMOnp4mIKpVEm/G/v4w8jvFEKw88h743vwaEIim88GEQItMxzGAV6zSqV1DWO\\nLxtoRsinslJYfAG46ex4YUATFveWvOUeWk5W1sEa5f3c0moaTmBM/PAAo8vLxhlw\\nJhsHihsCH+BcXKVMjW8OCqYYqISMxEifUBX63HcJt78ELHpOuc1c2eG59PomtTjQ\\nywIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[],\"attachment\":[{\"type\":\"PropertyValue\",\"name\":\"News\",\"value\":\"\\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Support\",\"value\":\"\\u003ca href=\\\"https://contact.framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003econtact.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Soutenir\",\"value\":\"\\u003ca href=\\\"https://soutenir.framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003esoutenir.framasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Site\",\"value\":\"\\u003ca href=\\\"https://framasoft.org/\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003eframasoft.org/\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"}],\"endpoints\":{\"sharedInbox\":\"https://framapiaf.org/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/000/002/original/85fbb27ad5e3cf71.jpg\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://framapiaf.s3.framasoft.org/framapiaf/accounts/headers/000/000/002/original/6aba75f1ab1ab6de.jpg\"}}", + "headers": { + "Date": "Fri, 06 Dec 2019 10:25:59 GMT", + "Content-Type": "application/activity+json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Server": "Mastodon", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "1; mode=block", + "Vary": "Accept, Accept-Encoding, Origin", + "Cache-Control": "max-age=180, public", + "ETag": "W/\"773e09a2a60446fe74d997858877f7e0\"", + "Set-Cookie": "_mastodon_session=U%2BUwfKRPF9LVzxKAjxaywz3ySEufApGuEhddpwvpJm7%2B0cDzsW0%2Fn64%2FwOLOYuE9SLOTCjU4Ufc3yaoLvdKx--x1HVRQfU7bAeHgaF--IV9oyi7ODNo19cAi%2FULzew%3D%3D; path=/; secure; HttpOnly", + "X-Request-Id": "f36578b7-2f1a-49d1-b2c4-0cc0d652160b", + "X-Runtime": "0.010705", + "X-Cached": "MISS", + "Strict-Transport-Security": "max-age=31536000" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/test/fixtures/vcr_cassettes/relay/fetch_relay_follow.json b/test/fixtures/vcr_cassettes/relay/fetch_relay_follow.json index 4f911b258..b477ec249 100644 --- a/test/fixtures/vcr_cassettes/relay/fetch_relay_follow.json +++ b/test/fixtures/vcr_cassettes/relay/fetch_relay_follow.json @@ -1,4 +1,36 @@ [ + { + "request": { + "body": "", + "headers": { + "Accept": "application/json, application/activity+json, application/jrd+json" + }, + "method": "get", + "options": { + "follow_redirect": "true" + }, + "request_body": "", + "url": "http://mobilizon1.com/.well-known/webfinger?resource=acct:relay@mobilizon1.com" + }, + "response": { + "binary": false, + "body": "{\"aliases\":[\"http://mobilizon1.com/relay\"],\"links\":[{\"href\":\"http://mobilizon1.com/relay\",\"rel\":\"self\",\"type\":\"application/activity+json\"},{\"href\":\"http://mobilizon1.com/relay\",\"rel\":\"https://webfinger.net/rel/profile-page/\",\"type\":\"text/html\"}],\"subject\":\"acct:relay@mobilizon1.com\"}", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Fri, 13 Dec 2019 09:41:40 GMT", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "284", + "Connection": "keep-alive", + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "access-control-expose-headers": "", + "cache-control": "max-age=0, private, must-revalidate", + "x-request-id": "Fd_k7OtPJ28p8-MAAAOh" + }, + "status_code": 200, + "type": "ok" + } + }, { "request": { "body": "", @@ -10,16 +42,22 @@ "follow_redirect": "true" }, "request_body": "", - "url": "http://localhost:8080/actor" + "url": "http://mobilizon1.com/relay" }, "response": { "binary": false, - "body": "{\"@context\": \"https://www.w3.org/ns/activitystreams\", \"endpoints\": {\"sharedInbox\": \"http://localhost:8080/inbox\"}, \"followers\": \"http://localhost:8080/followers\", \"following\": \"http://localhost:8080/following\", \"inbox\": \"http://localhost:8080/inbox\", \"name\": \"ActivityRelay\", \"type\": \"Application\", \"id\": \"http://localhost:8080/actor\", \"publicKey\": {\"id\": \"http://localhost:8080/actor#main-key\", \"owner\": \"http://localhost:8080/actor\", \"publicKeyPem\": \"-----BEGIN PUBLIC KEY-----\\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvs6UuAo26Sb3BiOK7xay\\nsBzqvXI3xd55JAP0pAk2faF+Vl3r67/g9MoND96JqCVMuzSJZ9oSsqa6ilJCxG3p\\nXUUfQUvqAMGW49cCvga86DG17Ennjbc4C6WIQtoW3Wm5OdDciPY2Dx+pSXdTOajB\\nFX6RHUZcgqHENrsm3jPZI138e/2OJeqdxv4/5t2xdPXEpWdPGitX9AJhrqPY4lzg\\nzQ9Y9wS2eS1CVL9vZZRf9Z4RiZvAfVb0s1iS/IUxrf4TYERRFJxEoDLD2SZVrkq6\\nvhGldCfw2ZnfTftA1ToXguC9S6nSaz+li0ajNjpK/xjZjlKvn0I078UPPe5LUlsb\\nUcYZvBx5PC5rV8yKMLlgxnTY8PqC8LEVc453wO7Ai4M5TeB0SUyEycZHSyLfvQXV\\nThEN/07u1UaJViY3U5S/SihyoCQUfJXQ3jx2SjGgM32/aJ3IwxgveLaTsaZ0VVKM\\nbawEFw6iAcWYM06hZSB6j6dkL1xh+FYGEQTPMYMqUOJi2r1cD8yMLe8dTFOmwMLt\\nBnf7xxvnjKJcv3e9zGRWIdLkQbBQn3BEuRTCUMgljipxdjbeE5/JSP1kQLB94ncb\\nb9gvYgtemJKvT8m37+HOi9MI4BMIlDwpRWjqPZmkNvkegR/1KPjJSsyAnGdd89ne\\np442vUqPyXIq0tSCDmjmU+cCAwEAAQ==\\n-----END PUBLIC KEY-----\"}, \"summary\": \"ActivityRelay bot\", \"preferredUsername\": \"relay\", \"url\": \"http://localhost:8080/actor\"}", + "body": "{\"@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\"}],\"endpoints\":{\"sharedInbox\":\"http://mobilizon1.com/inbox\"},\"followers\":\"http://mobilizon1.com/relay/followers\",\"following\":\"http://mobilizon1.com/relay/following\",\"id\":\"http://mobilizon1.com/relay\",\"inbox\":\"http://mobilizon1.com/inbox\",\"manuallyApprovesFollowers\":false,\"name\":\"Mobilizon\",\"outbox\":null,\"preferredUsername\":\"relay\",\"publicKey\":{\"id\":\"http://mobilizon1.com/relay#main-key\",\"owner\":\"http://mobilizon1.com/relay\",\"publicKeyPem\":\"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEAqBbeHMV5UVw0AIVch7fWDp2it5rqbGZX6yXPYnnT8LHhdvfv3DFk\\npk74BN66MzNqsthvSVznu2BEil0sEKD5rQoE9Yirhzz/LN9SlnU+u6262nBA18E3\\nkQ10RgL2jpZ9e8Om6qYqarhN7draupJXYRKEaUoEFPT09ABbwQv+4K1YadU8klJi\\nHJ6D+IIHiXNizfsxVLDKpbUKStMYeEzyfqCkWw0EQEuzc3O7Aci5lwCMkCts2993\\nsTbNyzsYAVWJNcy/An1F1P+K4iZhWEtZInQz67MBtjMWtQUhyWib0e671HdBiWM6\\nkZq74U8c6RR6eMzBLuY7YAUCG6nWg90zxwIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"},\"summary\":\"Change this to a proper description of your instance\",\"type\":\"Application\",\"url\":\"http://mobilizon1.com/relay\"}", "headers": { - "Content-Type": "application/json; charset=utf-8", - "Content-Length": "1368", - "Date": "Thu, 01 Aug 2019 14:44:38 GMT", - "Server": "Python/3.7 aiohttp/3.3.2" + "Server": "nginx/1.16.1", + "Date": "Fri, 13 Dec 2019 09:41:41 GMT", + "Content-Type": "application/activity+json", + "Content-Length": "1657", + "Connection": "keep-alive", + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "access-control-expose-headers": "", + "cache-control": "max-age=0, private, must-revalidate", + "x-request-id": "Fd_k7PjOAWuySL0AAAPB" }, "status_code": 200, "type": "ok" @@ -27,30 +65,36 @@ }, { "request": { - "body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://litepub.github.io/litepub/context.jsonld\",{\"Hashtag\":\"as:Hashtag\",\"category\":\"sc:category\",\"sc\":\"http://schema.org#\",\"uuid\":\"sc:identifier\"}],\"actor\":\"http://mobilizon.test/relay\",\"cc\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"id\":\"http://mobilizon.test/follow/69/activity\",\"object\":\"http://localhost:8080/actor\",\"to\":[\"http://localhost:8080/actor\"],\"type\":\"Follow\"}", + "body": "{\"@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://mobilizon.test/relay\",\"cc\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"id\":\"http://mobilizon.test/follow/b7791977-2a75-4715-815b-6e7125065b71\",\"object\":\"http://mobilizon1.com/relay\",\"to\":[\"http://mobilizon1.com/relay\"],\"type\":\"Follow\"}", "headers": { "Content-Type": "application/activity+json", - "signature": "keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"UADlb5eaeqmujO5zGfK1mWB3WZFXU6lkUgSvEf5YyQMOIkMaudDwTfNPIa4IYh2VMLwyYSjOOXxkcBdCw4f9UnMBQBhomPNRNkJ0QBzoxILPmyxddAojH9IzwwAUL/nHSGWaO116bkCux0OcEM5AVIrCT6dENep39lOjnOGPelBB5mKMS78AxH4pU/5tTGFKmNgiRL4Q06ezPUJHKauRrMwzcqZYdjUn+U9MDBDrYyfAzqQlgBPU/fMCjwusndxaICb9c+40YE3WaXzKewIivfrMoOBzWyw6ZsgAG8/NoOH+8z9Z+hBvdjCUXeG2bvAPPclNkSJillwIA2PnMOVgpw==\"", - "digest": "SHA-256=Ady0Dj2bEXe201P9bThLaj1Kw/7O1cfrjN9IifEfVBg=", - "date": "Thu, 01 Aug 2019 14:44:38 GMT" + "signature": "keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"WbyGHT/WdvdRpWek8uCGHrFSblLpg+Iq802R5S2cjNj035OKpxRmu1r8u9Qr5KGIKgZn6LHt9YmB+PNlwsubPtTSkJpE8AAUDMHLKgCrH7A5Q6x6GlARl5bHNo4QtOxkXvnEbn31xfNDNp70QqZb/emw95TnELYUlMLZds0qYutT8U4WdDhSWcVytQmKJWNZXxEj+KlMDUaxag3lGscJ/HY0F+yGNov7FHthid1Y4LTGFsp/tismnMTlba12NH/kXPHtduNsX8uxFslM2ODwqAaospTGEpXmr9CPgbNy7626qgYaR2RdB/fYlCayLI4JJIlH8gOdocGHPrWNtVEHaQ==\"", + "digest": "SHA-256=ibNFcsnBeCCjWZo9We60tKfbRN3el0WCMVdOxtuC1cg=", + "date": "Fri, 13 Dec 2019 09:41:41 GMT" }, "method": "post", "options": { "pool": "default" }, "request_body": "", - "url": "http://localhost:8080/inbox" + "url": "http://mobilizon1.com/inbox" }, "response": { "binary": false, - "body": "signature check failed, signature did not match key", + "body": "# HTTPoison.Error at POST /inbox\n\nException:\n\n ** (HTTPoison.Error) :nxdomain\n (httpoison) lib/httpoison.ex:128: HTTPoison.request!/5\n (mobilizon) lib/service/activity_pub/activity_pub.ex:610: Mobilizon.Service.ActivityPub.fetch_and_prepare_actor_from_url/1\n (mobilizon) lib/service/activity_pub/activity_pub.ex:473: Mobilizon.Service.ActivityPub.make_actor_from_url/2\n (mobilizon) lib/service/activity_pub/activity_pub.ex:122: Mobilizon.Service.ActivityPub.get_or_fetch_actor_by_url/2\n (mobilizon) lib/service/http_signatures/signature.ex:54: Mobilizon.Service.HTTPSignatures.Signature.get_public_key_for_url/1\n (mobilizon) lib/service/http_signatures/signature.ex:74: Mobilizon.Service.HTTPSignatures.Signature.fetch_public_key/1\n (http_signatures) lib/http_signatures/http_signatures.ex:40: HTTPSignatures.validate_conn/1\n (mobilizon) lib/mobilizon_web/http_signature.ex:45: MobilizonWeb.HTTPSignaturePlug.call/2\n (mobilizon) MobilizonWeb.Router.activity_pub_signature/2\n (mobilizon) lib/mobilizon_web/router.ex:1: MobilizonWeb.Router.__pipe_through7__/1\n (phoenix) lib/phoenix/router.ex:283: Phoenix.Router.__call__/2\n (mobilizon) lib/mobilizon_web/endpoint.ex:1: MobilizonWeb.Endpoint.plug_builder_call/2\n (mobilizon) lib/plug/debugger.ex:122: MobilizonWeb.Endpoint.\"call (overridable 3)\"/2\n (mobilizon) lib/mobilizon_web/endpoint.ex:1: MobilizonWeb.Endpoint.call/2\n (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4\n (cowboy) /home/tcit/dev/frama/mobilizon/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2\n (cowboy) /home/tcit/dev/frama/mobilizon/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3\n (cowboy) /home/tcit/dev/frama/mobilizon/deps/cowboy/src/cowboy_stream_h.erl:302: :cowboy_stream_h.request_process/3\n (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3\n \n\n## Connection details\n\n### Params\n\n %{\"@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://mobilizon.test/relay\", \"cc\" => [\"https://www.w3.org/ns/activitystreams#Public\"], \"id\" => \"http://mobilizon.test/follow/b7791977-2a75-4715-815b-6e7125065b71\", \"object\" => \"http://mobilizon1.com/relay\", \"to\" => [\"http://mobilizon1.com/relay\"], \"type\" => \"Follow\"}\n\n### Request info\n\n * URI: http://mobilizon1.com:80/inbox\n * Query string: \n\n### Headers\n \n * connection: upgrade\n * content-length: 912\n * content-type: application/activity+json\n * date: Fri, 13 Dec 2019 09:41:41 GMT\n * digest: SHA-256=ibNFcsnBeCCjWZo9We60tKfbRN3el0WCMVdOxtuC1cg=\n * host: mobilizon1.com\n * signature: keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"WbyGHT/WdvdRpWek8uCGHrFSblLpg+Iq802R5S2cjNj035OKpxRmu1r8u9Qr5KGIKgZn6LHt9YmB+PNlwsubPtTSkJpE8AAUDMHLKgCrH7A5Q6x6GlARl5bHNo4QtOxkXvnEbn31xfNDNp70QqZb/emw95TnELYUlMLZds0qYutT8U4WdDhSWcVytQmKJWNZXxEj+KlMDUaxag3lGscJ/HY0F+yGNov7FHthid1Y4LTGFsp/tismnMTlba12NH/kXPHtduNsX8uxFslM2ODwqAaospTGEpXmr9CPgbNy7626qgYaR2RdB/fYlCayLI4JJIlH8gOdocGHPrWNtVEHaQ==\"\n * user-agent: hackney/1.15.2\n * x-forwarded-for: 127.0.0.1\n * x-real-ip: 127.0.0.1\n\n### Session\n\n %{}\n", "headers": { - "Content-Length": "51", - "Content-Type": "text/plain; charset=utf-8", - "Date": "Thu, 01 Aug 2019 14:44:38 GMT", - "Server": "Python/3.7 aiohttp/3.3.2" + "Server": "nginx/1.16.1", + "Date": "Fri, 13 Dec 2019 09:41:41 GMT", + "Content-Type": "text/markdown; charset=utf-8", + "Content-Length": "3977", + "Connection": "keep-alive", + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "access-control-expose-headers": "", + "cache-control": "max-age=0, private, must-revalidate", + "x-request-id": "Fd_k7PoZpCCBYRQAAAPh" }, - "status_code": 401, + "status_code": 500, "type": "ok" } } diff --git a/test/fixtures/vcr_cassettes/relay/fetch_relay_unfollow.json b/test/fixtures/vcr_cassettes/relay/fetch_relay_unfollow.json index 0c2abfe16..7b743e63f 100644 --- a/test/fixtures/vcr_cassettes/relay/fetch_relay_unfollow.json +++ b/test/fixtures/vcr_cassettes/relay/fetch_relay_unfollow.json @@ -1,30 +1,33 @@ [ { "request": { - "body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://litepub.github.io/litepub/context.jsonld\",{\"Hashtag\":\"as:Hashtag\",\"category\":\"sc:category\",\"sc\":\"http://schema.org#\",\"uuid\":\"sc:identifier\"}],\"actor\":\"http://mobilizon.test/relay\",\"cc\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"id\":\"http://mobilizon.test/follow/68/activity\",\"object\":\"http://localhost:8080/actor\",\"to\":[\"http://localhost:8080/actor\"],\"type\":\"Follow\"}", + "body": "", "headers": { - "Content-Type": "application/activity+json", - "signature": "keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"WsxzipdObXsApVtY5l2yTonTOPV888XLKK2+AMQRyiNZm4RGMEux8kgBKgJIODaKmRx9EsX8dIzBtTmJdLyj5gqfjvGVyj8hVeR0ERNMZmjngh5EZ3W+ySbkdFYZeYDWhwpL1i+7dTFJ3zE/ASZVaTMeIgqEpFnzHNbamwPzBZVvcnzyraB1rrmwcbzzrk3UPlJ3tA+Xz67Njr2wOiNNsjZ53abArKZB3KGbife6OyrVrKldJ+UKZS+vokgUXFwvMBZxfdmH2GD+yXHPhCIu7bVu77ASdW7bl7tM3uIV/c/Wemy5qJtPOupwbDvpLZ9ETE5IRCoUPdQ7l75kvevNxQ==\"", - "digest": "SHA-256=qIEgTH6kBorFchTiX2kxd7onyZ7BHhvLgCODLs6RAVc=", - "date": "Thu, 01 Aug 2019 14:44:37 GMT" + "Accept": "application/json, application/activity+json, application/jrd+json" }, - "method": "post", + "method": "get", "options": { - "pool": "default" + "follow_redirect": "true" }, "request_body": "", - "url": "http://localhost:8080/inbox" + "url": "http://mobilizon1.com/.well-known/webfinger?resource=acct:relay@mobilizon1.com" }, "response": { "binary": false, - "body": "signature check failed, signature did not match key", + "body": "{\"aliases\":[\"http://mobilizon1.com/relay\"],\"links\":[{\"href\":\"http://mobilizon1.com/relay\",\"rel\":\"self\",\"type\":\"application/activity+json\"},{\"href\":\"http://mobilizon1.com/relay\",\"rel\":\"https://webfinger.net/rel/profile-page/\",\"type\":\"text/html\"}],\"subject\":\"acct:relay@mobilizon1.com\"}", "headers": { - "Content-Length": "51", - "Content-Type": "text/plain; charset=utf-8", - "Date": "Thu, 01 Aug 2019 14:44:37 GMT", - "Server": "Python/3.7 aiohttp/3.3.2" + "Server": "nginx/1.16.1", + "Date": "Fri, 13 Dec 2019 09:41:39 GMT", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "284", + "Connection": "keep-alive", + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "access-control-expose-headers": "", + "cache-control": "max-age=0, private, must-revalidate", + "x-request-id": "Fd_k7LmY5k0CMQkAAANB" }, - "status_code": 401, + "status_code": 200, "type": "ok" } }, @@ -39,19 +42,60 @@ "follow_redirect": "true" }, "request_body": "", - "url": "http://localhost:8080/actor" + "url": "http://mobilizon1.com/relay" }, "response": { "binary": false, - "body": "{\"@context\": \"https://www.w3.org/ns/activitystreams\", \"endpoints\": {\"sharedInbox\": \"http://localhost:8080/inbox\"}, \"followers\": \"http://localhost:8080/followers\", \"following\": \"http://localhost:8080/following\", \"inbox\": \"http://localhost:8080/inbox\", \"name\": \"ActivityRelay\", \"type\": \"Application\", \"id\": \"http://localhost:8080/actor\", \"publicKey\": {\"id\": \"http://localhost:8080/actor#main-key\", \"owner\": \"http://localhost:8080/actor\", \"publicKeyPem\": \"-----BEGIN PUBLIC KEY-----\\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvs6UuAo26Sb3BiOK7xay\\nsBzqvXI3xd55JAP0pAk2faF+Vl3r67/g9MoND96JqCVMuzSJZ9oSsqa6ilJCxG3p\\nXUUfQUvqAMGW49cCvga86DG17Ennjbc4C6WIQtoW3Wm5OdDciPY2Dx+pSXdTOajB\\nFX6RHUZcgqHENrsm3jPZI138e/2OJeqdxv4/5t2xdPXEpWdPGitX9AJhrqPY4lzg\\nzQ9Y9wS2eS1CVL9vZZRf9Z4RiZvAfVb0s1iS/IUxrf4TYERRFJxEoDLD2SZVrkq6\\nvhGldCfw2ZnfTftA1ToXguC9S6nSaz+li0ajNjpK/xjZjlKvn0I078UPPe5LUlsb\\nUcYZvBx5PC5rV8yKMLlgxnTY8PqC8LEVc453wO7Ai4M5TeB0SUyEycZHSyLfvQXV\\nThEN/07u1UaJViY3U5S/SihyoCQUfJXQ3jx2SjGgM32/aJ3IwxgveLaTsaZ0VVKM\\nbawEFw6iAcWYM06hZSB6j6dkL1xh+FYGEQTPMYMqUOJi2r1cD8yMLe8dTFOmwMLt\\nBnf7xxvnjKJcv3e9zGRWIdLkQbBQn3BEuRTCUMgljipxdjbeE5/JSP1kQLB94ncb\\nb9gvYgtemJKvT8m37+HOi9MI4BMIlDwpRWjqPZmkNvkegR/1KPjJSsyAnGdd89ne\\np442vUqPyXIq0tSCDmjmU+cCAwEAAQ==\\n-----END PUBLIC KEY-----\"}, \"summary\": \"ActivityRelay bot\", \"preferredUsername\": \"relay\", \"url\": \"http://localhost:8080/actor\"}", + "body": "{\"@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\"}],\"endpoints\":{\"sharedInbox\":\"http://mobilizon1.com/inbox\"},\"followers\":\"http://mobilizon1.com/relay/followers\",\"following\":\"http://mobilizon1.com/relay/following\",\"id\":\"http://mobilizon1.com/relay\",\"inbox\":\"http://mobilizon1.com/inbox\",\"manuallyApprovesFollowers\":false,\"name\":\"Mobilizon\",\"outbox\":null,\"preferredUsername\":\"relay\",\"publicKey\":{\"id\":\"http://mobilizon1.com/relay#main-key\",\"owner\":\"http://mobilizon1.com/relay\",\"publicKeyPem\":\"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEAqBbeHMV5UVw0AIVch7fWDp2it5rqbGZX6yXPYnnT8LHhdvfv3DFk\\npk74BN66MzNqsthvSVznu2BEil0sEKD5rQoE9Yirhzz/LN9SlnU+u6262nBA18E3\\nkQ10RgL2jpZ9e8Om6qYqarhN7draupJXYRKEaUoEFPT09ABbwQv+4K1YadU8klJi\\nHJ6D+IIHiXNizfsxVLDKpbUKStMYeEzyfqCkWw0EQEuzc3O7Aci5lwCMkCts2993\\nsTbNyzsYAVWJNcy/An1F1P+K4iZhWEtZInQz67MBtjMWtQUhyWib0e671HdBiWM6\\nkZq74U8c6RR6eMzBLuY7YAUCG6nWg90zxwIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"},\"summary\":\"Change this to a proper description of your instance\",\"type\":\"Application\",\"url\":\"http://mobilizon1.com/relay\"}", "headers": { - "Content-Type": "application/json; charset=utf-8", - "Content-Length": "1368", - "Date": "Thu, 01 Aug 2019 14:44:36 GMT", - "Server": "Python/3.7 aiohttp/3.3.2" + "Server": "nginx/1.16.1", + "Date": "Fri, 13 Dec 2019 09:41:40 GMT", + "Content-Type": "application/activity+json", + "Content-Length": "1657", + "Connection": "keep-alive", + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "access-control-expose-headers": "", + "cache-control": "max-age=0, private, must-revalidate", + "x-request-id": "Fd_k7L4h92fDp5cAAANh" }, "status_code": 200, "type": "ok" } + }, + { + "request": { + "body": "{\"@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://mobilizon.test/relay\",\"cc\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"id\":\"http://mobilizon.test/follow/57a6973e-f43f-4533-bf71-7a14a4c6e5ac\",\"object\":\"http://mobilizon1.com/relay\",\"to\":[\"http://mobilizon1.com/relay\"],\"type\":\"Follow\"}", + "headers": { + "Content-Type": "application/activity+json", + "signature": "keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"JQPqSiJ0ZYdU6llrYXNMuN/bfzoLyubwOB59bljFq6i8ORXLw62Pt7Jue5WkMsySFcCXgS8k8K/H81YZkKzfWadwQV9L5rQEFSuW/DYJ2xffsDj90GsSi+sDRaQ5Ke8nPEbEMGR9jalh/F2VL97XscCgm6i3tdpbs6aFmqjKC+LzeH665t0WCHUxTgK47wECrMHw3j7lteGdm6N6IKWoWsRYeJoyFr/QCbNdWQOaAYYpCbJd0fjhPQRHhWQXidBoaDkhwesWc3mO8pvEnply9ES7Nzc6ULK7B98hg+aWeep8/KzRbxFyJ0OgnDJj/l39QiJ9t7v0yHX/WUzn0CaiiQ==\"", + "digest": "SHA-256=Qc9d9X3qh2EqIqtn/72iY17OMDXAOINDC10hARNAc4w=", + "date": "Fri, 13 Dec 2019 09:41:40 GMT" + }, + "method": "post", + "options": { + "pool": "default" + }, + "request_body": "", + "url": "http://mobilizon1.com/inbox" + }, + "response": { + "binary": false, + "body": "# HTTPoison.Error at POST /inbox\n\nException:\n\n ** (HTTPoison.Error) :nxdomain\n (httpoison) lib/httpoison.ex:128: HTTPoison.request!/5\n (mobilizon) lib/service/activity_pub/activity_pub.ex:610: Mobilizon.Service.ActivityPub.fetch_and_prepare_actor_from_url/1\n (mobilizon) lib/service/activity_pub/activity_pub.ex:473: Mobilizon.Service.ActivityPub.make_actor_from_url/2\n (mobilizon) lib/service/activity_pub/activity_pub.ex:122: Mobilizon.Service.ActivityPub.get_or_fetch_actor_by_url/2\n (mobilizon) lib/service/http_signatures/signature.ex:54: Mobilizon.Service.HTTPSignatures.Signature.get_public_key_for_url/1\n (mobilizon) lib/service/http_signatures/signature.ex:74: Mobilizon.Service.HTTPSignatures.Signature.fetch_public_key/1\n (http_signatures) lib/http_signatures/http_signatures.ex:40: HTTPSignatures.validate_conn/1\n (mobilizon) lib/mobilizon_web/http_signature.ex:45: MobilizonWeb.HTTPSignaturePlug.call/2\n (mobilizon) MobilizonWeb.Router.activity_pub_signature/2\n (mobilizon) lib/mobilizon_web/router.ex:1: MobilizonWeb.Router.__pipe_through7__/1\n (phoenix) lib/phoenix/router.ex:283: Phoenix.Router.__call__/2\n (mobilizon) lib/mobilizon_web/endpoint.ex:1: MobilizonWeb.Endpoint.plug_builder_call/2\n (mobilizon) lib/plug/debugger.ex:122: MobilizonWeb.Endpoint.\"call (overridable 3)\"/2\n (mobilizon) lib/mobilizon_web/endpoint.ex:1: MobilizonWeb.Endpoint.call/2\n (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4\n (cowboy) /home/tcit/dev/frama/mobilizon/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2\n (cowboy) /home/tcit/dev/frama/mobilizon/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3\n (cowboy) /home/tcit/dev/frama/mobilizon/deps/cowboy/src/cowboy_stream_h.erl:302: :cowboy_stream_h.request_process/3\n (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3\n \n\n## Connection details\n\n### Params\n\n %{\"@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://mobilizon.test/relay\", \"cc\" => [\"https://www.w3.org/ns/activitystreams#Public\"], \"id\" => \"http://mobilizon.test/follow/57a6973e-f43f-4533-bf71-7a14a4c6e5ac\", \"object\" => \"http://mobilizon1.com/relay\", \"to\" => [\"http://mobilizon1.com/relay\"], \"type\" => \"Follow\"}\n\n### Request info\n\n * URI: http://mobilizon1.com:80/inbox\n * Query string: \n\n### Headers\n \n * connection: upgrade\n * content-length: 912\n * content-type: application/activity+json\n * date: Fri, 13 Dec 2019 09:41:40 GMT\n * digest: SHA-256=Qc9d9X3qh2EqIqtn/72iY17OMDXAOINDC10hARNAc4w=\n * host: mobilizon1.com\n * signature: keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"JQPqSiJ0ZYdU6llrYXNMuN/bfzoLyubwOB59bljFq6i8ORXLw62Pt7Jue5WkMsySFcCXgS8k8K/H81YZkKzfWadwQV9L5rQEFSuW/DYJ2xffsDj90GsSi+sDRaQ5Ke8nPEbEMGR9jalh/F2VL97XscCgm6i3tdpbs6aFmqjKC+LzeH665t0WCHUxTgK47wECrMHw3j7lteGdm6N6IKWoWsRYeJoyFr/QCbNdWQOaAYYpCbJd0fjhPQRHhWQXidBoaDkhwesWc3mO8pvEnply9ES7Nzc6ULK7B98hg+aWeep8/KzRbxFyJ0OgnDJj/l39QiJ9t7v0yHX/WUzn0CaiiQ==\"\n * user-agent: hackney/1.15.2\n * x-forwarded-for: 127.0.0.1\n * x-real-ip: 127.0.0.1\n\n### Session\n\n %{}\n", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Fri, 13 Dec 2019 09:41:40 GMT", + "Content-Type": "text/markdown; charset=utf-8", + "Content-Length": "3977", + "Connection": "keep-alive", + "access-control-allow-credentials": "true", + "access-control-allow-origin": "*", + "access-control-expose-headers": "", + "cache-control": "max-age=0, private, must-revalidate", + "x-request-id": "Fd_k7MU4jVIgj4wAAAOB" + }, + "status_code": 500, + "type": "ok" + } } ] \ No newline at end of file diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs index 1d065ae43..d829ecfd5 100644 --- a/test/mobilizon/actors/actors_test.exs +++ b/test/mobilizon/actors/actors_test.exs @@ -1,12 +1,13 @@ defmodule Mobilizon.ActorsTest do use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney - use Mobilizon.DataCase + use Oban.Testing, repo: Mobilizon.Storage.Repo import Mobilizon.Factory - alias Mobilizon.{Actors, Config, Users} + alias Mobilizon.{Actors, Config, Users, Events, Tombstone} alias Mobilizon.Actors.{Actor, Bot, Follower, Member} + alias Mobilizon.Events.{Event, Comment} alias Mobilizon.Media.File, as: FileModel alias Mobilizon.Service.ActivityPub alias Mobilizon.Storage.Page @@ -287,6 +288,12 @@ defmodule Mobilizon.ActorsTest do test "delete_actor/1 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) @@ -300,8 +307,34 @@ defmodule Mobilizon.ActorsTest do "/" <> banner_path ) - assert {:ok, %Actor{}} = Actors.delete_actor(actor) - assert_raise Ecto.NoResultsError, fn -> Actors.get_actor!(actor_id) end + assert {:ok, %Oban.Job{}} = Actors.delete_actor(actor) + + assert_enqueued( + worker: Mobilizon.Service.Workers.BackgroundWorker, + args: %{"actor_id" => actor.id, "op" => "delete_actor"} + ) + + assert %{success: 1, failure: 0} == Oban.drain_queue(:background) + + 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} = Events.get_comment(comment1.id) + refute is_nil(deleted_at) + assert %Tombstone{} = Tombstone.find_tombstone(comment1_url) + + refute File.exists?( + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + "/" <> avatar_path + ) refute File.exists?( Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> diff --git a/test/mobilizon/events/events_test.exs b/test/mobilizon/events/events_test.exs index bc0e4ba7a..5ecac1ae0 100644 --- a/test/mobilizon/events/events_test.exs +++ b/test/mobilizon/events/events_test.exs @@ -314,7 +314,10 @@ defmodule Mobilizon.EventsTest do setup do actor = insert(:actor) - event = insert(:event, organizer_actor: actor) + + event = + insert(:event, organizer_actor: actor, participant_stats: %{creator: 1, participant: 1}) + participant = insert(:participant, actor: actor, event: event) {:ok, participant: participant, event: event, actor: actor} end @@ -364,7 +367,8 @@ defmodule Mobilizon.EventsTest do test "update_participant/2 with invalid data returns error changeset", %{ participant: participant } do - assert {:error, %Ecto.Changeset{}} = Events.update_participant(participant, @invalid_attrs) + assert {:error, :participant, %Ecto.Changeset{}, %{}} = + Events.update_participant(participant, @invalid_attrs) end test "delete_participant/1 deletes the participant", %{participant: participant} do diff --git a/test/mobilizon/service/activity_pub/activity_pub_test.exs b/test/mobilizon/service/activity_pub/activity_pub_test.exs index 63f781677..db7683885 100644 --- a/test/mobilizon/service/activity_pub/activity_pub_test.exs +++ b/test/mobilizon/service/activity_pub/activity_pub_test.exs @@ -171,7 +171,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do assert update.data["actor"] == actor.url assert update.data["to"] == [@activity_pub_public_audience] assert update.data["object"]["id"] == actor.url - assert update.data["object"]["type"] == "Person" + assert update.data["object"]["type"] == :Person assert update.data["object"]["summary"] == @updated_actor_summary end diff --git a/test/mobilizon/service/activity_pub/converter/actor_test.exs b/test/mobilizon/service/activity_pub/converter/actor_test.exs index 88485ed70..edc8b0ee9 100644 --- a/test/mobilizon/service/activity_pub/converter/actor_test.exs +++ b/test/mobilizon/service/activity_pub/converter/actor_test.exs @@ -8,8 +8,8 @@ defmodule Mobilizon.Service.ActivityPub.Converter.ActorTest do test "valid actor to as" do data = ActorConverter.model_to_as(%Actor{type: :Person, preferred_username: "test_account"}) assert is_map(data) - assert data["type"] == "Person" - assert data["preferred_username"] == "test_account" + assert data["type"] == :Person + assert data["preferredUsername"] == "test_account" end end @@ -17,12 +17,13 @@ defmodule Mobilizon.Service.ActivityPub.Converter.ActorTest do test "valid as data to model" do {:ok, actor} = ActorConverter.as_to_model_data(%{ + "id" => "https://somedomain.tld/users/someone", "type" => "Person", "preferredUsername" => "test_account" }) - assert actor["type"] == :Person - assert actor["preferred_username"] == "test_account" + assert actor.type == "Person" + assert actor.preferred_username == "test_account" end end end diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index 995881ef4..afc191739 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -9,10 +9,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do use Mobilizon.DataCase import Mobilizon.Factory + import ExUnit.CaptureLog - alias Mobilizon.Actors + alias Mobilizon.{Actors, Events, Tombstone} alias Mobilizon.Actors.Actor - alias Mobilizon.Events alias Mobilizon.Events.{Comment, Event, Participant} alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.{Activity, Utils} @@ -131,7 +131,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do data |> Map.put("object", object) - assert ExUnit.CaptureLog.capture_log([level: :warn], fn -> + 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 @@ -145,7 +145,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert data["id"] == "https://framapiaf.org/users/admin/statuses/99512778738411822/activity" - assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + assert data["to"] == [ + "https://www.w3.org/ns/activitystreams#Public", + "https://framapiaf.org/users/tcit" + ] # assert data["cc"] == [ # "https://framapiaf.org/users/admin/followers", @@ -466,26 +469,70 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do refute is_nil(Events.get_comment_from_url(comment_url).deleted_at) end - # TODO : make me ASAP - # test "it fails for incoming deletes with spoofed origin" do - # activity = insert(:note_activity) + test "it fails for incoming deletes with spoofed origin" do + comment = insert(:comment) - # data = - # File.read!("test/fixtures/mastodon-delete.json") - # |> Jason.decode!() + announce_data = + File.read!("test/fixtures/mastodon-announce.json") + |> Jason.decode!() + |> Map.put("object", comment.url) - # object = - # data["object"] - # |> Map.put("id", activity.data["object"]["id"]) + {:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(announce_data) - # data = - # data - # |> Map.put("object", object) + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Jason.decode!() - # :error = Transmogrifier.handle_incoming(data) + object = + data["object"] + |> Map.put("id", comment.url) - # assert Repo.get(Activity, activity.id) - # end + data = + data + |> Map.put("object", object) + + :error = Transmogrifier.handle_incoming(data) + + assert Events.get_comment_from_url(comment.url) + end + + 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) + insert(:event, organizer_actor: actor) + + %Comment{url: comment1_url} = comment1 = insert(:comment, actor: actor) + insert(:comment, actor: actor) + + data = + File.read!("test/fixtures/mastodon-delete-user.json") + |> Poison.decode!() + + {:ok, _activity, _actor} = Transmogrifier.handle_incoming(data) + assert %{success: 1, failure: 0} == Oban.drain_queue(:background) + + assert {:ok, %Actor{suspended: true}} = Actors.get_actor_by_url(url) + assert {:error, :event_not_found} = Events.get_event(event1.id) + assert %Tombstone{} = Tombstone.find_tombstone(event1_url) + assert %Comment{deleted_at: deleted_at} = Events.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) + + data = + File.read!("test/fixtures/mastodon-delete-user.json") + |> Poison.decode!() + |> Map.put("actor", url) + + assert capture_log(fn -> + assert :error == Transmogrifier.handle_incoming(data) + end) =~ "Object origin check failed" + + assert Actors.get_actor_by_url(url) + end test "it works for incoming unannounces with an existing notice" do use_cassette "activity_pub/mastodon_unannounce_activity" do @@ -743,13 +790,14 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do end test "it accepts Flag activities" do - %Actor{url: reporter_url} = _reporter = insert(:actor) + %Actor{url: reporter_url} = Mobilizon.Service.ActivityPub.Relay.get_actor() %Actor{url: reported_url} = reported = insert(:actor) %Comment{url: comment_url} = _comment = insert(:comment, actor: reported) message = %{ "@context" => "https://www.w3.org/ns/activitystreams", + "to" => [], "cc" => [reported_url], "object" => [reported_url, comment_url], "type" => "Flag", @@ -762,11 +810,11 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert activity.data["object"] == [reported_url, comment_url] assert activity.data["content"] == "blocked AND reported!!!" assert activity.data["actor"] == reporter_url - assert activity.data["cc"] == [reported_url] + assert activity.data["cc"] == [] end test "it accepts Join activities" do - %Actor{url: _organizer_url} = organizer = insert(:actor) + %Actor{url: organizer_url} = organizer = insert(:actor) %Actor{url: participant_url} = _participant = insert(:actor) %Event{url: event_url} = _event = insert(:event, organizer_actor: organizer) @@ -779,8 +827,12 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert {:ok, activity, _} = Transmogrifier.handle_incoming(join_data) - assert activity.data["object"] == event_url - assert activity.data["actor"] == participant_url + 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["actor"] == organizer_url + assert activity.data["id"] =~ "/accept/join/" end test "it accepts Accept activities for Join activities" do @@ -821,12 +873,17 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do |> Map.put("object", participation.url) {:ok, reject_activity, _} = Transmogrifier.handle_incoming(reject_data) - assert reject_activity.data["object"] == join_activity.data["id"] - assert reject_activity.data["object"] =~ "/join/" + 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 :error == Transmogrifier.handle_incoming(reject_data) + 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 @@ -913,15 +970,6 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert Enum.member?(object["tag"], expected_mention) end - # test "it adds the sensitive property" do - # user = insert(:user) - - # {:ok, activity} = CommonAPI.post(user, %{"status" => "#nsfw hey"}) - # {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - # assert modified["object"]["sensitive"] - # end - test "it adds the json-ld context and the conversation property" do actor = insert(:actor) @@ -975,125 +1023,28 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert is_nil(modified["object"]["announcement_count"]) assert is_nil(modified["object"]["context_id"]) end + end - # describe "actor rewriting" do - # test "it fixes the actor URL property to be a proper URI" do - # data = %{ - # "url" => %{"href" => "http://example.com"} - # } + 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 + end - # rewritten = Transmogrifier.maybe_fix_user_object(data) - # assert rewritten["url"] == "http://example.com" - # end - # 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" + } - # describe "actor origin containment" do - # test "it rejects objects with a bogus origin" do - # {:error, _} = ActivityPub.fetch_object_from_id("https://info.pleroma.site/activity.json") - # end - - # test "it rejects activities which reference objects with bogus origins" do - # data = %{ - # "@context" => "https://www.w3.org/ns/activitystreams", - # "id" => "http://mastodon.example.org/users/admin/activities/1234", - # "actor" => "http://mastodon.example.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 - - # test "it rejects objects when attributedTo is wrong (variant 1)" do - # {:error, _} = ActivityPub.fetch_object_from_id("https://info.pleroma.site/activity2.json") - # end - - # test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do - # data = %{ - # "@context" => "https://www.w3.org/ns/activitystreams", - # "id" => "http://mastodon.example.org/users/admin/activities/1234", - # "actor" => "http://mastodon.example.org/users/admin", - # "to" => ["https://www.w3.org/ns/activitystreams#Public"], - # "object" => "https://info.pleroma.site/activity2.json", - # "type" => "Announce" - # } - - # :error = Transmogrifier.handle_incoming(data) - # end - - # test "it rejects objects when attributedTo is wrong (variant 2)" do - # {:error, _} = ActivityPub.fetch_object_from_id("https://info.pleroma.site/activity3.json") - # end - - # test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do - # data = %{ - # "@context" => "https://www.w3.org/ns/activitystreams", - # "id" => "http://mastodon.example.org/users/admin/activities/1234", - # "actor" => "http://mastodon.example.org/users/admin", - # "to" => ["https://www.w3.org/ns/activitystreams#Public"], - # "object" => "https://info.pleroma.site/activity3.json", - # "type" => "Announce" - # } - - # :error = Transmogrifier.handle_incoming(data) - # end - # end - - # describe "general origin containment" do - # test "contain_origin_from_id() catches obvious spoofing attempts" do - # data = %{ - # "id" => "http://example.com/~alyssa/activities/1234.json" - # } - - # :error = - # Transmogrifier.contain_origin_from_id( - # "http://example.org/~alyssa/activities/1234.json", - # data - # ) - # end - - # test "contain_origin_from_id() allows alternate IDs within the same origin domain" do - # data = %{ - # "id" => "http://example.com/~alyssa/activities/1234.json" - # } - - # :ok = - # Transmogrifier.contain_origin_from_id( - # "http://example.com/~alyssa/activities/1234", - # data - # ) - # end - - # test "contain_origin_from_id() allows matching IDs" do - # data = %{ - # "id" => "http://example.com/~alyssa/activities/1234.json" - # } - - # :ok = - # Transmogrifier.contain_origin_from_id( - # "http://example.com/~alyssa/activities/1234.json", - # data - # ) - # end - - # test "users cannot be collided through fake direction spoofing attempts" do - # user = - # insert(:user, %{ - # nickname: "rye@niu.moe", - # local: false, - # ap_id: "https://niu.moe/users/rye", - # follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) - # }) - - # {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") - # end - - # test "all objects with fake directions are rejected by the object fetcher" do - # {:error, _} = - # ActivityPub.fetch_and_contain_remote_object_from_id( - # "https://info.pleroma.site/activity4.json" - # ) - # end + :error = Transmogrifier.handle_incoming(data) + end + end end end diff --git a/test/mobilizon/service/activity_pub/utils_test.exs b/test/mobilizon/service/activity_pub/utils_test.exs index 572792e5c..9cf960e1a 100644 --- a/test/mobilizon/service/activity_pub/utils_test.exs +++ b/test/mobilizon/service/activity_pub/utils_test.exs @@ -5,7 +5,7 @@ defmodule Mobilizon.Service.ActivityPub.UtilsTest do import Mobilizon.Factory - alias Mobilizon.Service.ActivityPub.{Converter, Utils} + alias Mobilizon.Service.ActivityPub.Converter alias MobilizonWeb.Endpoint alias MobilizonWeb.Router.Helpers, as: Routes @@ -36,7 +36,8 @@ defmodule Mobilizon.Service.ActivityPub.UtilsTest do "uuid" => reply.uuid, "id" => Routes.page_url(Endpoint, :comment, reply.uuid), "inReplyTo" => comment.url, - "attributedTo" => reply.actor.url + "attributedTo" => reply.actor.url, + "mediaType" => "text/html" } == Converter.Comment.model_to_as(reply) end @@ -44,7 +45,7 @@ defmodule Mobilizon.Service.ActivityPub.UtilsTest do comment = insert(:comment) reply = insert(:comment, in_reply_to_comment: comment) to = ["https://www.w3.org/ns/activitystreams#Public"] - comment_data = Utils.make_comment_data(reply.actor.url, to, reply.text, comment.url) + comment_data = Converter.Comment.model_to_as(reply) assert comment_data["type"] == "Note" assert comment_data["to"] == to assert comment_data["content"] == reply.text diff --git a/test/mobilizon/service/formatter/formatter_test.exs b/test/mobilizon/service/formatter/formatter_test.exs index c1194a4fc..4948543e9 100644 --- a/test/mobilizon/service/formatter/formatter_test.exs +++ b/test/mobilizon/service/formatter/formatter_test.exs @@ -33,21 +33,21 @@ defmodule Mobilizon.Service.FormatterTest do text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ." expected = - "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ." + "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ." assert {^expected, [], []} = Formatter.linkify(text) text = "https://mastodon.social/@lambadalambda" expected = - "https://mastodon.social/@lambadalambda" + "https://mastodon.social/@lambadalambda" assert {^expected, [], []} = Formatter.linkify(text) text = "https://mastodon.social:4000/@lambadalambda" expected = - "https://mastodon.social:4000/@lambadalambda" + "https://mastodon.social:4000/@lambadalambda" assert {^expected, [], []} = Formatter.linkify(text) @@ -59,56 +59,57 @@ defmodule Mobilizon.Service.FormatterTest do text = "http://www.cs.vu.nl/~ast/intel/" expected = - "http://www.cs.vu.nl/~ast/intel/" + "http://www.cs.vu.nl/~ast/intel/" assert {^expected, [], []} = Formatter.linkify(text) text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" expected = - "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" + "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" assert {^expected, [], []} = Formatter.linkify(text) text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" expected = - "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" + "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" assert {^expected, [], []} = Formatter.linkify(text) text = "https://www.google.co.jp/search?q=Nasim+Aghdam" expected = - "https://www.google.co.jp/search?q=Nasim+Aghdam" + "https://www.google.co.jp/search?q=Nasim+Aghdam" assert {^expected, [], []} = Formatter.linkify(text) text = "https://en.wikipedia.org/wiki/Duff's_device" expected = - "https://en.wikipedia.org/wiki/Duff's_device" + "https://en.wikipedia.org/wiki/Duff's_device" assert {^expected, [], []} = Formatter.linkify(text) text = "https://pleroma.com https://pleroma.com/sucks" expected = - "https://pleroma.comhttps://pleroma.com/sucks" + "https://pleroma.comhttps://pleroma.com/sucks" assert {^expected, [], []} = Formatter.linkify(text) text = "xmpp:contact@hacktivis.me" expected = - "xmpp:contact@hacktivis.me" + "xmpp:contact@hacktivis.me" assert {^expected, [], []} = Formatter.linkify(text) text = "magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com" - expected = "#{text}" + expected = + "#{text}" assert {^expected, [], []} = Formatter.linkify(text) end @@ -117,32 +118,36 @@ defmodule Mobilizon.Service.FormatterTest do describe "add_user_links" do test "gives a replacement for user links, using local nicknames in user links text" do text = "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme@archae.me" - _gsimg = insert(:actor, preferred_username: "gsimg") + gsimg = insert(:actor, preferred_username: "gsimg") - _archaeme = + archaeme = insert(:actor, preferred_username: "archa_eme_", url: "https://archeme/@archa_eme_") - _archaeme_remote = insert(:actor, preferred_username: "archaeme", domain: "archae.me") + archaeme_remote = insert(:actor, preferred_username: "archaeme", domain: "archae.me") {text, mentions, []} = Formatter.linkify(text) assert length(mentions) == 3 expected_text = - "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme" + "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme" assert expected_text == text end test "gives a replacement for single-character local nicknames" do text = "@o hi" - _o = insert(:actor, preferred_username: "o") + o = insert(:actor, preferred_username: "o") {text, mentions, []} = Formatter.linkify(text) assert length(mentions) == 1 - expected_text = "@o hi" + expected_text = "@o hi" assert expected_text == text end diff --git a/test/mobilizon_web/api/report_test.exs b/test/mobilizon_web/api/report_test.exs index e2e80c083..db7d3f9bd 100644 --- a/test/mobilizon_web/api/report_test.exs +++ b/test/mobilizon_web/api/report_test.exs @@ -14,7 +14,8 @@ defmodule MobilizonWeb.API.ReportTest do describe "reports" do test "creates a report on a event" do - %Actor{id: reporter_id, url: reporter_url} = insert(:actor) + %Actor{url: relay_reporter_url} = Mobilizon.Service.ActivityPub.Relay.get_actor() + %Actor{id: reporter_id} = insert(:actor) %Actor{id: reported_id, url: reported_url} = reported = insert(:actor) %Event{id: event_id, url: event_url} = _event = insert(:event, organizer_actor: reported) @@ -28,11 +29,11 @@ defmodule MobilizonWeb.API.ReportTest do content: comment, event_id: event_id, comments_ids: [], - local: true + forward: false }) assert %Activity{ - actor: ^reporter_url, + actor: ^relay_reporter_url, data: %{ "type" => "Flag", "cc" => [], @@ -43,7 +44,8 @@ defmodule MobilizonWeb.API.ReportTest do end test "creates a report on several comments" do - %Actor{id: reporter_id, url: reporter_url} = insert(:actor) + %Actor{url: relay_reporter_url} = Mobilizon.Service.ActivityPub.Relay.get_actor() + %Actor{id: reporter_id} = insert(:actor) %Actor{id: reported_id, url: reported_url} = reported = insert(:actor) %Comment{id: comment_1_id, url: comment_1_url} = @@ -64,20 +66,21 @@ defmodule MobilizonWeb.API.ReportTest do }) assert %Activity{ - actor: ^reporter_url, + actor: ^relay_reporter_url, data: %{ "type" => "Flag", "content" => ^comment, "object" => [^reported_url, ^comment_1_url, ^comment_2_url], - "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "to" => [], "cc" => [], - "actor" => ^reporter_url + "actor" => ^relay_reporter_url } } = flag_activity end test "creates a report that gets federated" do - %Actor{id: reporter_id, url: reporter_url} = insert(:actor) + %Actor{url: relay_reporter_url} = Mobilizon.Service.ActivityPub.Relay.get_actor() + %Actor{id: reporter_id} = insert(:actor) %Actor{id: reported_id, url: reported_url} = reported = insert(:actor) %Comment{id: comment_1_id, url: comment_1_url} = @@ -96,21 +99,21 @@ defmodule MobilizonWeb.API.ReportTest do content: comment, event_id: nil, comments_ids: [comment_1_id, comment_2_id], - local: false + forward: true }) assert %Activity{ - actor: ^reporter_url, + actor: ^relay_reporter_url, data: %{ "type" => "Flag", - "actor" => ^reporter_url, + "actor" => ^relay_reporter_url, "cc" => [^reported_url], "content" => ^encoded_comment, "object" => [^reported_url, ^comment_1_url, ^comment_2_url], - "to" => ["https://www.w3.org/ns/activitystreams#Public"] + "to" => [] }, local: true, - recipients: ["https://www.w3.org/ns/activitystreams#Public", ^reported_url] + recipients: [^reported_url] } = flag_activity end diff --git a/test/mobilizon_web/controllers/activity_pub_controller_test.exs b/test/mobilizon_web/controllers/activity_pub_controller_test.exs index 30e1f67ae..6c1b9a3d2 100644 --- a/test/mobilizon_web/controllers/activity_pub_controller_test.exs +++ b/test/mobilizon_web/controllers/activity_pub_controller_test.exs @@ -19,6 +19,11 @@ defmodule MobilizonWeb.ActivityPubControllerTest do alias MobilizonWeb.PageView alias MobilizonWeb.Router.Helpers, as: Routes + setup_all do + Mobilizon.Config.put([:instance, :federating], true) + :ok + end + setup do conn = build_conn() |> put_req_header("accept", "application/activity+json") {:ok, conn: conn} @@ -34,7 +39,10 @@ defmodule MobilizonWeb.ActivityPubControllerTest do actor = Actors.get_actor!(actor.id) - assert json_response(conn, 200) == ActorView.render("actor.json", %{actor: actor}) + assert json_response(conn, 200) == + ActorView.render("actor.json", %{actor: actor}) + |> Jason.encode!() + |> Jason.decode!() end end diff --git a/test/mobilizon_web/controllers/webfinger_controller_test.exs b/test/mobilizon_web/controllers/webfinger_controller_test.exs index e97db6f4c..d980d1be9 100644 --- a/test/mobilizon_web/controllers/webfinger_controller_test.exs +++ b/test/mobilizon_web/controllers/webfinger_controller_test.exs @@ -9,6 +9,11 @@ defmodule MobilizonWeb.WebFingerTest do alias Mobilizon.Service.WebFinger import Mobilizon.Factory + setup_all do + Mobilizon.Config.put([:instance, :federating], true) + :ok + end + test "GET /.well-known/host-meta", %{conn: conn} do conn = get(conn, "/.well-known/host-meta") diff --git a/test/mobilizon_web/plugs/federating_plug_test.exs b/test/mobilizon_web/plugs/federating_plug_test.exs new file mode 100644 index 000000000..74aa72ae5 --- /dev/null +++ b/test/mobilizon_web/plugs/federating_plug_test.exs @@ -0,0 +1,30 @@ +# Portions of this file are derived from Pleroma: +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule MobilizonWeb.Plug.FederatingTest do + use MobilizonWeb.ConnCase + + test "returns and halt the conn when federating is disabled" do + Mobilizon.Config.put([:instance, :federating], false) + + conn = + build_conn() + |> MobilizonWeb.Plugs.Federating.call(%{}) + + assert conn.status == 404 + assert conn.halted + end + + test "does nothing when federating is enabled" do + Mobilizon.Config.put([:instance, :federating], true) + + conn = + build_conn() + |> MobilizonWeb.Plugs.Federating.call(%{}) + + refute conn.status + refute conn.halted + end +end diff --git a/test/mobilizon_web/plugs/mapped_identity_to_signature_plug_test.exs b/test/mobilizon_web/plugs/mapped_identity_to_signature_plug_test.exs new file mode 100644 index 000000000..9af71da9d --- /dev/null +++ b/test/mobilizon_web/plugs/mapped_identity_to_signature_plug_test.exs @@ -0,0 +1,60 @@ +# Portions of this file are derived from Pleroma: +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule MobilizonWeb.Plugs.MappedSignatureToIdentityPlugTest do + use MobilizonWeb.ConnCase + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + alias MobilizonWeb.Plugs.MappedSignatureToIdentity + + defp set_signature(conn, key_id) do + conn + |> put_req_header("signature", "keyId=\"#{key_id}\"") + |> assign(:valid_signature, true) + end + + test "it successfully maps a valid identity with a valid signature" do + use_cassette "activity_pub/signature/valid" do + conn = + build_conn(:get, "/doesntmattter") + |> set_signature("https://framapiaf.org/users/admin") + |> MappedSignatureToIdentity.call(%{}) + + refute is_nil(conn.assigns.actor) + end + end + + test "it successfully maps a valid identity with a valid signature with payload" do + use_cassette "activity_pub/signature/valid_payload" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "https://framapiaf.org/users/admin"}) + |> set_signature("https://framapiaf.org/users/admin") + |> MappedSignatureToIdentity.call(%{}) + + refute is_nil(conn.assigns.actor) + end + end + + test "it considers a mapped identity to be invalid when it mismatches a payload" do + use_cassette "activity_pub/signature/invalid_payload" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "https://framapiaf.org/users/admin"}) + |> set_signature("https://niu.moe/users/rye") + |> MappedSignatureToIdentity.call(%{}) + + assert %{valid_signature: false} == conn.assigns + end + end + + test "it considers a mapped identity to be invalid when the identity cannot be found" do + use_cassette "activity_pub/signature/invalid_not_found" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "https://framapiaf.org/users/admin"}) + |> set_signature("http://niu.moe/users/rye") + |> MappedSignatureToIdentity.call(%{}) + + assert %{valid_signature: false} == conn.assigns + end + end +end diff --git a/test/mobilizon_web/resolvers/admin_resolver_test.exs b/test/mobilizon_web/resolvers/admin_resolver_test.exs index a6a079b3b..7e6cf996e 100644 --- a/test/mobilizon_web/resolvers/admin_resolver_test.exs +++ b/test/mobilizon_web/resolvers/admin_resolver_test.exs @@ -121,4 +121,99 @@ defmodule MobilizonWeb.Resolvers.AdminResolverTest do title end end + + describe "Resolver: Get the list of relay followers" do + test "test list_relay_followers/3 returns relay followers", %{conn: conn} do + %User{} = user_admin = insert(:user, role: :administrator) + + follower_actor = + insert(:actor, + domain: "localhost", + user: nil, + url: "http://localhost:8080/actor", + preferred_username: "instance_actor", + name: "I am an instance actor" + ) + + %Actor{} = relay_actor = Mobilizon.Service.ActivityPub.Relay.get_actor() + insert(:follower, actor: follower_actor, target_actor: relay_actor) + + query = """ + { + relayFollowers { + elements { + actor { + preferredUsername, + domain, + }, + approved + }, + total + } + } + """ + + res = + conn + |> auth_conn(user_admin) + |> AbsintheHelpers.graphql_query(query: query) + + assert is_nil(res["errors"]) + + assert hd(res["data"]["relayFollowers"]["elements"]) == %{ + "actor" => %{"preferredUsername" => "instance_actor", "domain" => "localhost"}, + "approved" => false + } + end + + test "test list_relay_followers/3 returns relay followings", %{conn: conn} do + %User{} = user_admin = insert(:user, role: :administrator) + + %Actor{ + preferred_username: following_actor_preferred_username, + domain: following_actor_domain + } = + following_actor = + insert(:actor, + domain: "localhost", + user: nil, + url: "http://localhost:8080/actor", + preferred_username: "instance_actor", + name: "I am an instance actor" + ) + + %Actor{} = relay_actor = Mobilizon.Service.ActivityPub.Relay.get_actor() + insert(:follower, actor: relay_actor, target_actor: following_actor) + + query = """ + { + relayFollowings { + elements { + targetActor { + preferredUsername, + domain, + }, + approved + }, + total + } + } + """ + + res = + conn + |> auth_conn(user_admin) + |> AbsintheHelpers.graphql_query(query: query) + + assert is_nil(res["errors"]) + + assert hd(res["data"]["relayFollowings"]["elements"]) == %{ + "targetActor" => %{ + "preferredUsername" => following_actor_preferred_username, + "domain" => following_actor_domain + }, + "approved" => false + } + end + end end diff --git a/test/mobilizon_web/resolvers/participant_resolver_test.exs b/test/mobilizon_web/resolvers/participant_resolver_test.exs index 5e0639669..7aadfd4a5 100644 --- a/test/mobilizon_web/resolvers/participant_resolver_test.exs +++ b/test/mobilizon_web/resolvers/participant_resolver_test.exs @@ -199,7 +199,9 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do user: user, actor: actor } do - event = insert(:event, %{organizer_actor: actor}) + event = + insert(:event, %{organizer_actor: actor, participant_stats: %{creator: 1, participant: 1}}) + insert(:participant, %{actor: actor, event: event, role: :creator}) user2 = insert(:user) actor2 = insert(:actor, user: user2) diff --git a/test/mobilizon_web/resolvers/person_resolver_test.exs b/test/mobilizon_web/resolvers/person_resolver_test.exs index b8cd88d73..e0e7ae609 100644 --- a/test/mobilizon_web/resolvers/person_resolver_test.exs +++ b/test/mobilizon_web/resolvers/person_resolver_test.exs @@ -3,6 +3,7 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do alias MobilizonWeb.AbsintheHelpers alias Mobilizon.Actors.Actor import Mobilizon.Factory + use Oban.Testing, repo: Mobilizon.Storage.Repo @non_existent_username "nonexistent" @@ -478,7 +479,7 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do "Cannot remove the last administrator of a group" end - test "delete_person/3 should delete a user identity", context do + test "delete_person/3 should delete an actor identity", context do user = insert(:user) %Actor{id: person_id} = insert(:actor, user: user, preferred_username: "riri") insert(:actor, user: user, preferred_username: "fifi") @@ -498,6 +499,13 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do assert json_response(res, 200)["errors"] == nil + assert_enqueued( + worker: Mobilizon.Service.Workers.BackgroundWorker, + args: %{"actor_id" => person_id, "op" => "delete_actor"} + ) + + assert %{success: 1, failure: 0} == Oban.drain_queue(:background) + query = """ { person(id: "#{person_id}") { diff --git a/test/support/abinthe_helpers.ex b/test/support/abinthe_helpers.ex index 33df27dac..d9c8a0c12 100644 --- a/test/support/abinthe_helpers.ex +++ b/test/support/abinthe_helpers.ex @@ -25,7 +25,7 @@ defmodule MobilizonWeb.AbsintheHelpers do conn |> post( "/api", - build_query(options[:query], options[:variables]) + build_query(options[:query], Keyword.get(options, :variables, %{})) ) |> json_response(200) end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index 26ee7cb2c..cdcedddb0 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -14,14 +14,14 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do describe "running follow" do test "relay is followed" do use_cassette "relay/fetch_relay_follow" do - target_instance = "http://localhost:8080/actor" + target_instance = "mobilizon1.com" Mix.Tasks.Mobilizon.Relay.run(["follow", target_instance]) local_actor = Relay.get_actor() assert local_actor.url =~ "/relay" - {:ok, target_actor} = Actors.get_actor_by_url(target_instance) + {:ok, target_actor} = Actors.get_actor_by_url("http://#{target_instance}/relay") refute is_nil(target_actor.domain) assert Actors.is_following(local_actor, target_actor) @@ -32,12 +32,15 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do describe "running unfollow" do test "relay is unfollowed" do use_cassette "relay/fetch_relay_unfollow" do - target_instance = "http://localhost:8080/actor" + target_instance = "mobilizon1.com" Mix.Tasks.Mobilizon.Relay.run(["follow", target_instance]) %Actor{} = local_actor = Relay.get_actor() - {:ok, %Actor{} = target_actor} = Actors.get_actor_by_url(target_instance) + + {:ok, %Actor{} = target_actor} = + Actors.get_actor_by_url("http://#{target_instance}/relay") + assert %Follower{} = Actors.is_following(local_actor, target_actor) Mix.Tasks.Mobilizon.Relay.run(["unfollow", target_instance])