Merge branch 'feature/admin-instance-follow' into 'master'
Add admin interface to manage instances subscriptions See merge request framasoft/mobilizon!337
This commit is contained in:
commit
4470bf5c9a
1
.gitignore
vendored
1
.gitignore
vendored
@ -19,6 +19,7 @@ erl_crash.dump
|
||||
.env.test
|
||||
/.env
|
||||
.env.2
|
||||
.env.1
|
||||
|
||||
/setup_db.psql
|
||||
|
||||
|
@ -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.
|
||||
|
104
docs/contribute/activity_pub.md
Normal file
104
docs/contribute/activity_pub.md
Normal file
@ -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"
|
||||
}
|
||||
```
|
@ -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",
|
||||
@ -58,7 +65,7 @@
|
||||
"@vue/cli-plugin-unit-mocha": "^4.0.3",
|
||||
"@vue/cli-service": "^4.0.3",
|
||||
"@vue/eslint-config-typescript": "^5.0.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"@vue/test-utils": "^1.0.0-beta.30",
|
||||
"apollo-link-error": "^1.1.12",
|
||||
"chai": "^4.2.0",
|
||||
"dotenv-webpack": "^1.7.0",
|
||||
|
@ -26,7 +26,8 @@
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<span class="title" ref="title">{{ actorDisplayName }}</span><br>
|
||||
<small class="has-text-grey">@{{ participant.actor.preferredUsername }}</small>
|
||||
<small class="has-text-grey" v-if="participant.actor.domain">@{{ participant.actor.preferredUsername }}@{{ participant.actor.domain }}</small>
|
||||
<small class="has-text-grey" v-else>@{{ participant.actor.preferredUsername }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -41,7 +42,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IActor, IPerson, Person } from '@/types/actor';
|
||||
import { Person } from '@/types/actor';
|
||||
import { IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
|
||||
@Component
|
||||
|
141
js/src/components/Admin/Followers.vue
Normal file
141
js/src/components/Admin/Followers.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table
|
||||
v-show="relayFollowers.elements.length > 0"
|
||||
:data="relayFollowers.elements"
|
||||
:loading="$apollo.queries.relayFollowers.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
:is-row-checkable="(row) => row.id !== 3"
|
||||
detailed
|
||||
:show-detail-icon="false"
|
||||
paginated
|
||||
backend-pagination
|
||||
:total="relayFollowers.total"
|
||||
:per-page="perPage"
|
||||
@page-change="onPageChange"
|
||||
checkable
|
||||
checkbox-position="left">
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="actor.id" label="ID" width="40" numeric>
|
||||
{{ props.row.actor.id }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="actor.type" :label="$t('Type')" width="80">
|
||||
<b-icon icon="lan" v-if="isInstance(props.row.actor)" />
|
||||
<b-icon icon="account-circle" v-else />
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="approved" :label="$t('Status')" width="100" sortable centered>
|
||||
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger' }`">
|
||||
{{ props.row.approved ? $t('Accepted') : $t('Pending') }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
|
||||
<template>
|
||||
<a @click="toggle(props.row)" v-if="isInstance(props.row.actor)">
|
||||
{{ props.row.actor.domain }}
|
||||
</a>
|
||||
<a @click="toggle(props.row)" v-else>
|
||||
{{ `${props.row.actor.preferredUsername}@${props.row.actor.domain}` }}
|
||||
</a>
|
||||
</template>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="actor.updatedAt" :label="$t('Date')" sortable>
|
||||
{{ props.row.updatedAt | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
|
||||
<template slot="detail" slot-scope="props">
|
||||
<article>
|
||||
<div class="content">
|
||||
<strong>{{ props.row.actor.domain }}</strong>
|
||||
<small>@{{ props.row.actor.preferredUsername }}</small>
|
||||
<small>31m</small>
|
||||
<br>
|
||||
<p v-html="props.row.actor.summary" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<div class="buttons">
|
||||
<b-button @click="acceptRelays" type="is-success" v-if="checkedRowsHaveAtLeastOneToApprove">
|
||||
{{ $tc('No instance to approve|Approve instance|Approve {number} instances', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
<b-button @click="rejectRelays" type="is-danger">
|
||||
{{ $tc('No instance to reject|Reject instance|Reject {number} instances', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-message type="is-danger" v-if="relayFollowers.elements.length === 0">
|
||||
{{ $t("No instance follows your instance yet.") }}
|
||||
</b-message>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from 'vue-property-decorator';
|
||||
import { ACCEPT_RELAY, REJECT_RELAY, RELAY_FOLLOWERS } from '@/graphql/admin';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
import { IFollower } from '@/types/actor/follower.model';
|
||||
import RelayMixin from '@/mixins/relay';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowers: {
|
||||
query: RELAY_FOLLOWERS,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('Followers') as string,
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Followers extends Mixins(RelayMixin) {
|
||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
async acceptRelays() {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.acceptRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async rejectRelays() {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.rejectRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async acceptRelay(address: String) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ACCEPT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
async rejectRelay(address: String) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REJECT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
get checkedRowsHaveAtLeastOneToApprove(): boolean {
|
||||
return this.checkedRows.some(checkedRow => !checkedRow.approved);
|
||||
}
|
||||
}
|
||||
</script>
|
142
js/src/components/Admin/Followings.vue
Normal file
142
js/src/components/Admin/Followings.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="followRelay">
|
||||
<b-field :label="$t('Add an instance')" custom-class="add-relay" horizontal>
|
||||
<b-field grouped expanded size="is-large">
|
||||
<p class="control">
|
||||
<b-input v-model="newRelayAddress" :placeholder="$t('Ex: test.mobilizon.org')" />
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{ $t('Add an instance') }}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</form>
|
||||
<b-table
|
||||
v-show="relayFollowings.elements.length > 0"
|
||||
:data="relayFollowings.elements"
|
||||
:loading="$apollo.queries.relayFollowings.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
:is-row-checkable="(row) => row.id !== 3"
|
||||
detailed
|
||||
:show-detail-icon="false"
|
||||
paginated
|
||||
backend-pagination
|
||||
:total="relayFollowings.total"
|
||||
:per-page="perPage"
|
||||
@page-change="onPageChange"
|
||||
checkable
|
||||
checkbox-position="left">
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="targetActor.id" label="ID" width="40" numeric>
|
||||
{{ props.row.targetActor.id }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="targetActor.type" :label="$t('Type')" width="80">
|
||||
<b-icon icon="lan" v-if="isInstance(props.row.targetActor)" />
|
||||
<b-icon icon="account-circle" v-else />
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="approved" :label="$t('Status')" width="100" sortable centered>
|
||||
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger' }`">
|
||||
{{ props.row.approved ? $t('Accepted') : $t('Pending') }}
|
||||
</span>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
|
||||
<template>
|
||||
<a @click="toggle(props.row)" v-if="isInstance(props.row.targetActor)">
|
||||
{{ props.row.targetActor.domain }}
|
||||
</a>
|
||||
<a @click="toggle(props.row)" v-else>
|
||||
{{ `${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}` }}
|
||||
</a>
|
||||
</template>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable>
|
||||
{{ props.row.updatedAt | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
|
||||
<template slot="detail" slot-scope="props">
|
||||
<article>
|
||||
<div class="content">
|
||||
<strong>{{ props.row.targetActor.domain }}</strong>
|
||||
<small>@{{ props.row.targetActor.preferredUsername }}</small>
|
||||
<small>31m</small>
|
||||
<br>
|
||||
<p v-html="props.row.targetActor.summary" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<b-button @click="removeRelays" type="is-danger">
|
||||
{{ $tc('No instance to remove|Remove instance|Remove {number} instances', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-message type="is-danger" v-if="relayFollowings.elements.length === 0">
|
||||
{{ $t("You don't follow any instances yet.") }}
|
||||
</b-message>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from 'vue-property-decorator';
|
||||
import { ADD_RELAY, RELAY_FOLLOWINGS, REMOVE_RELAY } from '@/graphql/admin';
|
||||
import { IFollower } from '@/types/actor/follower.model';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
import RelayMixin from '@/mixins/relay';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowings: {
|
||||
query: RELAY_FOLLOWINGS,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('Followings') as string,
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Followings extends Mixins(RelayMixin) {
|
||||
|
||||
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
newRelayAddress: String = '';
|
||||
|
||||
async followRelay(e) {
|
||||
e.preventDefault();
|
||||
await this.$apollo.mutate({
|
||||
mutation: ADD_RELAY,
|
||||
variables: {
|
||||
address: this.newRelayAddress,
|
||||
},
|
||||
// TODO: Handle cache update properly without refreshing
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.newRelayAddress = '';
|
||||
}
|
||||
|
||||
async removeRelays() {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.removeRelay(`${row.targetActor.preferredUsername}@${row.targetActor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async removeRelay(address: String) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REMOVE_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.checkedRows = [];
|
||||
}
|
||||
}
|
||||
</script>
|
@ -11,7 +11,8 @@
|
||||
<div class="content">
|
||||
<span class="first-line" v-if="!comment.deletedAt">
|
||||
<strong>{{ comment.actor.name }}</strong>
|
||||
<small>@{{ comment.actor.preferredUsername }}</small>
|
||||
<small v-if="comment.actor.domain">@{{ comment.actor.preferredUsername }}@{{ comment.actor.domain }}</small>
|
||||
<small v-else>@{{ comment.actor.preferredUsername }}</small>
|
||||
<a class="comment-link has-text-grey" :href="commentId">
|
||||
<small>{{ timeago(new Date(comment.updatedAt)) }}</small>
|
||||
</a>
|
||||
@ -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({
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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}`;
|
||||
|
@ -26,11 +26,11 @@ A button to set your participation
|
||||
<div class="participation-button">
|
||||
<b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">
|
||||
<button class="button is-success" type="button" slot="trigger">
|
||||
<b-icon icon="check"></b-icon>
|
||||
<b-icon icon="check" />
|
||||
<template>
|
||||
<span>{{ $t('I participate') }}</span>
|
||||
</template>
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
|
||||
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
|
||||
@ -45,11 +45,11 @@ A button to set your participation
|
||||
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
|
||||
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
|
||||
<button class="button is-success" type="button" slot="trigger">
|
||||
<b-icon icon="timer-sand-empty"></b-icon>
|
||||
<b-icon icon="timer-sand-empty" />
|
||||
<template>
|
||||
<span>{{ $t('I participate') }}</span>
|
||||
</template>
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
|
||||
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
|
||||
@ -73,7 +73,7 @@ A button to set your participation
|
||||
<template>
|
||||
<span>{{ $t('Participate') }}</span>
|
||||
</template>
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
|
||||
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
|
||||
@ -84,12 +84,12 @@ A button to set your participation
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<span>{{ $t('with {identity}', {identity: currentActor.preferredUsername }) }}</span>
|
||||
<span>{{ $t('as {identity}', {identity: currentActor.preferredUsername }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item :value="false" aria-role="listitem" @click="joinModal">
|
||||
<b-dropdown-item :value="false" aria-role="listitem" @click="joinModal" v-if="identities.length > 1">
|
||||
{{ $t('with another identity…')}}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
@ -99,14 +99,32 @@ A button to set your participation
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import { IDENTITIES } from '@/graphql/actor';
|
||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { ICurrentUser } from '@/types/current-user.model';
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
apollo: {
|
||||
currentUser: {
|
||||
query: CURRENT_USER_CLIENT,
|
||||
},
|
||||
identities: {
|
||||
query: IDENTITIES,
|
||||
update: ({ identities }) => identities ? identities.map(identity => new Person(identity)) : [],
|
||||
skip() {
|
||||
return this.currentUser.isLoggedIn === false;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class ParticipationButton extends Vue {
|
||||
@Prop({ required: true }) participation!: IParticipant;
|
||||
@Prop({ required: true }) currentActor!: IPerson;
|
||||
|
||||
ParticipantRole = ParticipantRole;
|
||||
currentUser!: ICurrentUser;
|
||||
identities: IPerson[] = [];
|
||||
|
||||
joinEvent(actor: IPerson) {
|
||||
this.$emit('joinEvent', actor);
|
||||
|
@ -20,7 +20,14 @@
|
||||
</div>
|
||||
|
||||
<div class="content columns">
|
||||
<div class="column is-one-quarter-desktop">Reported by <img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}</div>
|
||||
<div class="column is-one-quarter-desktop">
|
||||
<span v-if="report.reporter.type === ActorType.APPLICATION">
|
||||
{{ $t('Reported by someone on {domain}', { domain: report.reporter.domain}) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('Reported by {reporter}', { reporter: report.reporter.preferredUsername}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="column" v-if="report.content">{{ report.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -29,10 +36,13 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IReport } from '@/types/report.model';
|
||||
import { ActorType } from '@/types/actor';
|
||||
|
||||
@Component
|
||||
export default class ReportCard extends Vue {
|
||||
@Prop({ required: true }) report!: IReport;
|
||||
|
||||
ActorType = ActorType;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
@ -44,11 +44,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="outsideDomain">
|
||||
{{ $t('The content came from another server. Transfer an anonymous copy of the report?') }}
|
||||
</p>
|
||||
|
||||
<div class="control" v-if="outsideDomain">
|
||||
<p>{{ $t('The content came from another server. Transfer an anonymous copy of the report?') }}</p>
|
||||
<b-switch v-model="forward">{{ $t('Transfer to {outsideDomain}', { outsideDomain }) }}</b-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,3 +19,87 @@ export const DASHBOARD = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RELAY_FRAGMENT = gql`
|
||||
fragment relayFragment on Follower {
|
||||
actor {
|
||||
id,
|
||||
preferredUsername,
|
||||
name,
|
||||
domain,
|
||||
type,
|
||||
summary
|
||||
},
|
||||
targetActor {
|
||||
id,
|
||||
preferredUsername,
|
||||
name,
|
||||
domain,
|
||||
type,
|
||||
summary
|
||||
},
|
||||
approved,
|
||||
insertedAt,
|
||||
updatedAt
|
||||
}
|
||||
`;
|
||||
|
||||
export const RELAY_FOLLOWERS = gql`
|
||||
query relayFollowers($page: Int, $limit: Int) {
|
||||
relayFollowers(page: $page, limit: $limit) {
|
||||
elements {
|
||||
...relayFragment
|
||||
},
|
||||
total
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const RELAY_FOLLOWINGS = gql`
|
||||
query relayFollowings($page: Int, $limit: Int) {
|
||||
relayFollowings(page: $page, limit: $limit) {
|
||||
elements {
|
||||
...relayFragment
|
||||
},
|
||||
total
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const ADD_RELAY = gql`
|
||||
mutation addRelay($address: String!) {
|
||||
addRelay(address: $address) {
|
||||
...relayFragment
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const REMOVE_RELAY = gql`
|
||||
mutation removeRelay($address: String!) {
|
||||
removeRelay(address: $address) {
|
||||
...relayFragment
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const ACCEPT_RELAY = gql`
|
||||
mutation acceptRelay($address: String!) {
|
||||
acceptRelay(address: $address) {
|
||||
...relayFragment
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const REJECT_RELAY = gql`
|
||||
mutation rejectRelay($address: String!) {
|
||||
rejectRelay(address: $address) {
|
||||
...relayFragment
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
@ -13,6 +13,7 @@ export const COMMENT_FIELDS_FRAGMENT = gql`
|
||||
url
|
||||
},
|
||||
id,
|
||||
domain,
|
||||
preferredUsername,
|
||||
name
|
||||
},
|
||||
|
@ -10,7 +10,8 @@ const participantQuery = `
|
||||
url
|
||||
},
|
||||
name,
|
||||
id
|
||||
id,
|
||||
domain
|
||||
},
|
||||
event {
|
||||
id
|
||||
@ -441,3 +442,21 @@ export const EVENT_PERSON_PARTICIPATION = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql`
|
||||
subscription ($actorId: ID!, $eventId: ID!) {
|
||||
eventPersonParticipationChanged(personId: $actorId) {
|
||||
id,
|
||||
participations(eventId: $eventId) {
|
||||
id,
|
||||
role,
|
||||
actor {
|
||||
id
|
||||
},
|
||||
event {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -18,7 +18,9 @@ export const REPORTS = gql`
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
domain,
|
||||
type
|
||||
},
|
||||
event {
|
||||
id,
|
||||
@ -52,7 +54,9 @@ const REPORT_FRAGMENT = gql`
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
domain,
|
||||
type
|
||||
},
|
||||
event {
|
||||
id,
|
||||
@ -111,9 +115,10 @@ export const CREATE_REPORT = gql`
|
||||
$reporterId: ID!,
|
||||
$reportedId: ID!,
|
||||
$content: String,
|
||||
$commentsIds: [ID]
|
||||
$commentsIds: [ID],
|
||||
$forward: Boolean
|
||||
) {
|
||||
createReport(eventId: $eventId, reporterId: $reporterId, reportedId: $reportedId, content: $content, commentsIds: $commentsIds) {
|
||||
createReport(eventId: $eventId, reporterId: $reporterId, reportedId: $reportedId, content: $content, commentsIds: $commentsIds, forward: $forward) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +322,6 @@
|
||||
"resend confirmation email": "Bestätigungsmail erneut senden",
|
||||
"respect of the fundamental freedoms": "Respekt für die fundamentalen Freiheiten",
|
||||
"with another identity…": "mit einer anderen Identität.…",
|
||||
"with {identity}": "mit {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} Plätze",
|
||||
"{count} participants": "Noch keine Teilnehmer | Ein Teilnehmer | {count} Teilnehmer",
|
||||
"{count} requests waiting": "{count} Anfragen ausstehend",
|
||||
|
@ -333,11 +333,59 @@
|
||||
"resend confirmation email": "resend confirmation email",
|
||||
"respect of the fundamental freedoms": "respect of the fundamental freedoms",
|
||||
"with another identity…": "with another identity…",
|
||||
"with {identity}": "with {identity}",
|
||||
"as {identity}": "as {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} seats",
|
||||
"{count} participants": "No participants yet | One participant | {count} participants",
|
||||
"{count} requests waiting": "{count} requests waiting",
|
||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.",
|
||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks",
|
||||
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors"
|
||||
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors",
|
||||
"Reply": "Reply",
|
||||
"Accepted": "Accepted",
|
||||
"Pending": "Pending",
|
||||
"No instance to remove|Remove instance|Remove {number} instances": "No instances to remove|Remove instance|Remove {number} instances",
|
||||
"Dashboard": "Dashboard",
|
||||
"Reports": "Reports",
|
||||
"Mark as resolved": "Mark as resolved",
|
||||
"Reopen": "Reopen",
|
||||
"Close": "Close",
|
||||
"Reported identity": "Reported identity",
|
||||
"Reported by": "Reported by",
|
||||
"Reported": "Reported",
|
||||
"Updated": "Updated",
|
||||
"Open": "Open",
|
||||
"Closed": "Closed",
|
||||
"Resolved": "Resolved",
|
||||
"Unknown": "Unknown",
|
||||
"No comment": "No comment",
|
||||
"Notes": "Notes",
|
||||
"New note": "New note",
|
||||
"Add a note": "Add a note",
|
||||
"Deleting event": "Deleting event",
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.",
|
||||
"Delete Event": "Delete Event",
|
||||
"Type": "Type",
|
||||
"Domain": "Domain",
|
||||
"Date": "Date",
|
||||
"No instance to approve|Approve instance|Approve {number} instances": "No instance to approve|Approve instance|Approve {number} instances",
|
||||
"No instance to reject|Reject instance|Reject {number} instances": "No instance to reject|Reject instance|Reject {number} instances",
|
||||
"No instance follows your instance yet.": "No instance follows your instance yet.",
|
||||
"Followers": "Followers",
|
||||
"Add an instance": "Add an instance",
|
||||
"Ex: test.mobilizon.org": "Ex: test.mobilizon.org",
|
||||
"You don't follow any instances yet.": "You don't follow any instances yet.",
|
||||
"Followings": "Followings",
|
||||
"Instances": "Instances",
|
||||
"Reported by {reporter}": "Reported by {reporter}",
|
||||
"No open reports yet": "No open reports yet",
|
||||
"No resolved reports yet": "No resolved reports yet",
|
||||
"No closed reports yet": "No closed reports yet",
|
||||
"Reported by someone on {domain}": "Reported by someone on {domain}",
|
||||
"Your participation has been rejected": "Your participation has been rejected",
|
||||
"Your participation status has been changed": "Your participation status has been changed",
|
||||
"Unknown actor": "Unknown actor",
|
||||
"Deleting comment": "Deleting comment",
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Are you sure you want to <b>delete</b> this comment? This action cannot be undone.",
|
||||
"Delete Comment": "Delete Comment",
|
||||
"Comment deleted": "Comment deleted"
|
||||
}
|
@ -286,7 +286,7 @@
|
||||
"Update my event": "Éditer mon événement",
|
||||
"User accounts and every other data is currently deleted every 48 hours, so you may want to register again.": "Les comptes utilisateurs et toutes les autres données sont actuellement supprimées toutes les 48 heures, donc vous voulez peut-être vous inscrire à nouveau.",
|
||||
"Username": "Pseudo",
|
||||
"Users": "Utilisateurs",
|
||||
"Users": "Utilisateur⋅ice⋅s",
|
||||
"View a reply": "Aucune réponse | Voir une réponse | Voir {totalReplies} réponses",
|
||||
"View event page": "Voir la page de l'événement",
|
||||
"View everything": "Voir tout",
|
||||
@ -337,11 +337,57 @@
|
||||
"resend confirmation email": "réenvoyer l'email de confirmation",
|
||||
"respect of the fundamental freedoms": "le respect des libertés fondamentales",
|
||||
"with another identity…": "avec une autre identité…",
|
||||
"with {identity}": "avec {identity}",
|
||||
"as {identity}": "en tant que {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} places",
|
||||
"{count} participants": "Aucun⋅e participant⋅e | Un⋅e participant⋅e | {count} participant⋅e⋅s",
|
||||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.",
|
||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"Reply": "Répondre",
|
||||
"Accepted": "Accepté",
|
||||
"Pending": "En attente",
|
||||
"No instance to remove|Remove instance|Remove {number} instances": "Pas d'instances à supprimer|Supprimer une instance|Supprimer {number} instances",
|
||||
"Mark as resolved": "Marquer comme résolu",
|
||||
"Reopen": "Réouvrir",
|
||||
"Close": "Fermé",
|
||||
"Reported identity": "Identité signalée",
|
||||
"Reported by": "Signalée par",
|
||||
"Reported": "Signalée",
|
||||
"Updated": "Mis à jour",
|
||||
"Open": "Ouvert",
|
||||
"Closed": "Fermé",
|
||||
"Resolved": "Résolu",
|
||||
"Unknown": "Inconnu",
|
||||
"No comment": "Pas de commentaire",
|
||||
"Notes": "Notes",
|
||||
"New note": "Nouvelle note",
|
||||
"Add a note": "Ajouter une note",
|
||||
"Deleting event": "Suppression de l'événement",
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> cet événement ? Cette action n'est pas réversible. Vous voulez peut-être engager la conversation avec le créateur de l'événement ou bien éditer son événement à la place.",
|
||||
"Delete Event": "Supprimer l'événement",
|
||||
"Type": "Type",
|
||||
"Domain": "Domaine",
|
||||
"Date": "Date",
|
||||
"No instance to approve|Approve instance|Approve {number} instances": "Aucune instance à approuver|Approuver une instance|Approuver {number} instances",
|
||||
"No instance to reject|Reject instance|Reject {number} instances": "Aucune instance à rejetter|Rejetter une instance|Rejetter {number} instances",
|
||||
"No instance follows your instance yet.": "Aucune instance ne suit votre instance pour le moment.",
|
||||
"Followers": "Abonnés",
|
||||
"Add an instance": "Ajouter une instance",
|
||||
"Ex: test.mobilizon.org": "Ex: test.mobilizon.org",
|
||||
"You don't follow any instances yet.": "Vous ne suivez aucune instance pour le moment.",
|
||||
"Followings": "Abonnements",
|
||||
"Instances": "Instances",
|
||||
"Reported by {reporter}": "Signalé par {reporter}",
|
||||
"No open reports yet": "Aucun signalement ouvert pour le moment",
|
||||
"No resolved reports yet": "Aucun signalement résolu pour le moment",
|
||||
"No closed reports yet": "Aucun signalement fermé pour le moment",
|
||||
"Reported by someone on {domain}": "Signalé par quelqu'un depuis {domain}",
|
||||
"Your participation has been rejected": "Votre participation a été rejettée",
|
||||
"Your participation status has been changed": "Le statut de votre participation a été mis à jour",
|
||||
"Unknown actor": "Acteur inconnu",
|
||||
"Deleting comment": "Suppression du commentaire en cours",
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> ce commentaire? Cette action ne peut pas être annulée.",
|
||||
"Delete Comment": "Supprimer le commentaire",
|
||||
"Comment deleted": "Commentaire supprimé"
|
||||
}
|
@ -321,7 +321,6 @@
|
||||
"resend confirmation email": "bevestigingsemail opnieuw versturen",
|
||||
"respect of the fundamental freedoms": "respect voor de fundamentele vrijheden",
|
||||
"with another identity…": "met een andere identiteit…",
|
||||
"with {identity}": "met {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} plaatsen",
|
||||
"{count} participants": "Nog geen deelnemers | Eén deelnemer | {count} deelnemers",
|
||||
"{count} requests waiting": "{count} aanvragen in afwachting",
|
||||
|
@ -368,7 +368,6 @@
|
||||
"resend confirmation email": "tornar enviar lo messatge de confirmacion",
|
||||
"respect of the fundamental freedoms": "lo respet de las libertats fondamentalas",
|
||||
"with another identity…": "amb una autra identitat…",
|
||||
"with {identity}": "amb {identity}",
|
||||
"{actor}'s avatar": "Avatar de {actor}",
|
||||
"{approved} / {total} seats": "{approved} / {total} plaças",
|
||||
"{count} participants": "Cap de participacion pel moment|Un participant|{count} participants",
|
||||
|
@ -324,7 +324,6 @@
|
||||
"resend confirmation email": "skicka bekräftelsemail igen",
|
||||
"respect of the fundamental freedoms": "respektera våra grundläggande friheter",
|
||||
"with another identity…": "med en annan identitet…",
|
||||
"with {identity}": "med {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} platser",
|
||||
"{count} participants": "Inga deltagande ännu|En deltagande|{count} deltagande",
|
||||
"{count} requests waiting": "{count} förfrågningar väntar",
|
||||
|
44
js/src/mixins/relay.ts
Normal file
44
js/src/mixins/relay.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Refs } from '@/shims-vue';
|
||||
import { ActorType, IActor } from '@/types/actor';
|
||||
import { IFollower } from '@/types/actor/follower.model';
|
||||
|
||||
@Component
|
||||
export default class RelayMixin extends Vue {
|
||||
$refs!: Refs<{
|
||||
table: any,
|
||||
}>;
|
||||
|
||||
checkedRows: IFollower[] = [];
|
||||
page: number = 1;
|
||||
perPage: number = 2;
|
||||
|
||||
toggle(row) {
|
||||
this.$refs.table.toggleDetails(row);
|
||||
}
|
||||
|
||||
async onPageChange(page: number) {
|
||||
this.page = page;
|
||||
await this.$apollo.queries.relayFollowings.fetchMore({
|
||||
variables: {
|
||||
page: this.page,
|
||||
limit: this.perPage,
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newFollowings = fetchMoreResult.relayFollowings.elements;
|
||||
return {
|
||||
relayFollowings: {
|
||||
__typename: previousResult.relayFollowings.__typename,
|
||||
total: previousResult.relayFollowings.total,
|
||||
elements: [...previousResult.relayFollowings.elements, ...newFollowings],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
isInstance(actor: IActor): boolean {
|
||||
return actor.type === ActorType.APPLICATION && actor.preferredUsername === 'relay';
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import Vue from 'vue';
|
||||
import { ColorModifiers } from 'buefy/types/helpers';
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$notifier: {
|
||||
success: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -17,21 +19,23 @@ export class Notifier {
|
||||
}
|
||||
|
||||
success(message: string) {
|
||||
this.vue.prototype.$buefy.notification.open({
|
||||
message,
|
||||
duration: 5000,
|
||||
position: 'is-bottom-right',
|
||||
type: 'is-success',
|
||||
hasIcon: true,
|
||||
});
|
||||
this.notification(message, 'is-success');
|
||||
}
|
||||
|
||||
error(message: string) {
|
||||
this.notification(message, 'is-danger');
|
||||
}
|
||||
|
||||
info(message: string) {
|
||||
this.notification(message, 'is-info');
|
||||
}
|
||||
|
||||
private notification(message: string, type: ColorModifiers) {
|
||||
this.vue.prototype.$buefy.notification.open({
|
||||
message,
|
||||
duration: 5000,
|
||||
position: 'is-bottom-right',
|
||||
type: 'is-danger',
|
||||
type,
|
||||
hasIcon: true,
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { RouteConfig } from 'vue-router';
|
||||
import Dashboard from '@/views/Admin/Dashboard.vue';
|
||||
import Follows from '@/views/Admin/Follows.vue';
|
||||
import Followings from '@/components/Admin/Followings.vue';
|
||||
import Followers from '@/components/Admin/Followers.vue';
|
||||
|
||||
export enum AdminRouteName {
|
||||
DASHBOARD = 'Dashboard',
|
||||
RELAYS = 'Relays',
|
||||
RELAY_FOLLOWINGS = 'Followings',
|
||||
RELAY_FOLLOWERS = 'Followers',
|
||||
}
|
||||
|
||||
export const adminRoutes: RouteConfig[] = [
|
||||
@ -13,4 +19,24 @@ export const adminRoutes: RouteConfig[] = [
|
||||
props: true,
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/admin/relays',
|
||||
name: AdminRouteName.RELAYS,
|
||||
redirect: { name: AdminRouteName.RELAY_FOLLOWINGS },
|
||||
component: Follows,
|
||||
children: [
|
||||
{
|
||||
path: 'followings',
|
||||
name: AdminRouteName.RELAY_FOLLOWINGS,
|
||||
component: Followings,
|
||||
},
|
||||
{
|
||||
path: 'followers',
|
||||
name: AdminRouteName.RELAY_FOLLOWERS,
|
||||
component: Followers,
|
||||
},
|
||||
],
|
||||
props: true,
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
];
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { IPicture } from '@/types/picture.model';
|
||||
|
||||
export enum ActorType {
|
||||
PERSON = 'PERSON',
|
||||
APPLICATION = 'APPLICATION',
|
||||
GROUP = 'GROUP',
|
||||
ORGANISATION = 'ORGANISATION',
|
||||
SERVICE = 'SERVICE',
|
||||
}
|
||||
|
||||
export interface IActor {
|
||||
id?: number;
|
||||
url: string;
|
||||
@ -10,6 +18,7 @@ export interface IActor {
|
||||
suspended: boolean;
|
||||
avatar: IPicture | null;
|
||||
banner: IPicture | null;
|
||||
type: ActorType;
|
||||
}
|
||||
|
||||
export class Actor implements IActor {
|
||||
@ -22,6 +31,7 @@ export class Actor implements IActor {
|
||||
summary: string = '';
|
||||
suspended: boolean = false;
|
||||
url: string = '';
|
||||
type: ActorType = ActorType.PERSON;
|
||||
|
||||
constructor (hash: IActor | {} = {}) {
|
||||
Object.assign(this, hash);
|
||||
|
8
js/src/types/actor/follower.model.ts
Normal file
8
js/src/types/actor/follower.model.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { IActor } from '@/types/actor/actor.model';
|
||||
|
||||
export interface IFollower {
|
||||
id?: string;
|
||||
actor: IActor;
|
||||
targetActor: IActor;
|
||||
approved: boolean;
|
||||
}
|
@ -242,7 +242,7 @@ export class EventModel implements IEvent {
|
||||
|
||||
this.onlineAddress = hash.onlineAddress;
|
||||
this.phoneAddress = hash.phoneAddress;
|
||||
this.physicalAddress = new Address(hash.physicalAddress);
|
||||
this.physicalAddress = hash.physicalAddress ? new Address(hash.physicalAddress) : undefined;
|
||||
this.participantStats = hash.participantStats;
|
||||
|
||||
this.tags = hash.tags;
|
||||
|
4
js/src/types/paginate.ts
Normal file
4
js/src/types/paginate.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Paginate<T> {
|
||||
elements: T[];
|
||||
total: number;
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
<div class="list is-hoverable">
|
||||
<a class="list-item" v-for="identity in identities" :class="{ 'is-active': identity.id === currentIdentity.id }" @click="changeCurrentIdentity(identity)">
|
||||
<div class="media">
|
||||
<img class="media-left image" v-if="identity.avatar" :src="identity.avatar.url" alt="" />
|
||||
<img class="media-left image is-48x48" v-if="identity.avatar" :src="identity.avatar.url" alt="" />
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<div class="media-content">
|
||||
<h3>@{{ identity.preferredUsername }}</h3>
|
||||
@ -17,7 +17,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<slot name="footer"></slot>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div class="identity-picker">
|
||||
<span v-if="inline" class="inline">
|
||||
<img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/> {{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
|
||||
<img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/>
|
||||
<b-icon v-else size="is-small" icon="account-circle" />
|
||||
{{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
|
||||
<b-button type="is-text" @click="isComponentModalActive = true">
|
||||
{{ $t('Change') }}
|
||||
</b-button>
|
||||
|
@ -38,6 +38,13 @@
|
||||
</article>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<router-link :to="{ name: RouteName.RELAYS }">
|
||||
<article class="tile is-child box">
|
||||
<p class="subtitle">{{ $t('Instances') }}</p>
|
||||
</article>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<article class="tile is-child box">
|
||||
@ -67,6 +74,12 @@ import { RouteName } from '@/router';
|
||||
query: DASHBOARD,
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('Administration') as string,
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Dashboard extends Vue {
|
||||
dashboard!: IDashboard;
|
||||
|
57
js/src/views/Admin/Follows.vue
Normal file
57
js/src/views/Admin/Follows.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1 class="title">{{ $t('Instances') }}</h1>
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<router-link tag="li" active-class="is-active" :to="{name: RouteName.RELAY_FOLLOWINGS}" exact>
|
||||
<a>
|
||||
<b-icon icon="inbox-arrow-down"></b-icon>
|
||||
<span>{{ $t('Followings') }} <b-tag rounded> {{ relayFollowings.total }} </b-tag> </span>
|
||||
</a>
|
||||
</router-link>
|
||||
<router-link tag="li" active-class="is-active" :to="{name: RouteName.RELAY_FOLLOWERS}" exact>
|
||||
<a>
|
||||
<b-icon icon="inbox-arrow-up"></b-icon>
|
||||
<span>{{ $t('Followers') }} <b-tag rounded> {{ relayFollowers.total }} </b-tag> </span>
|
||||
</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { RouteName } from '@/router';
|
||||
import { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from '@/graphql/admin';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
import { IFollower } from '@/types/actor/follower.model';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowings: {
|
||||
query: RELAY_FOLLOWINGS,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
relayFollowers: {
|
||||
query: RELAY_FOLLOWERS,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Follows extends Vue {
|
||||
RouteName = RouteName;
|
||||
activeTab: number = 0;
|
||||
|
||||
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.tab-item {
|
||||
form {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -29,7 +29,7 @@
|
||||
<address-auto-complete v-model="event.physicalAddress" />
|
||||
|
||||
<b-field :label="$t('Organizer')">
|
||||
<identity-picker-wrapper v-model="event.organizerActor"></identity-picker-wrapper>
|
||||
<identity-picker-wrapper v-model="event.organizerActor" />
|
||||
</b-field>
|
||||
|
||||
<div class="field">
|
||||
@ -92,7 +92,7 @@
|
||||
|
||||
<div class="box" v-if="limitedPlaces">
|
||||
<b-field :label="$t('Number of places')">
|
||||
<b-numberinput controls-position="compact" min="0" v-model="event.options.maximumAttendeeCapacity"></b-numberinput>
|
||||
<b-numberinput controls-position="compact" min="0" v-model="event.options.maximumAttendeeCapacity" />
|
||||
</b-field>
|
||||
<!--
|
||||
<b-field>
|
||||
@ -145,21 +145,21 @@
|
||||
name="status"
|
||||
type="is-warning"
|
||||
:native-value="EventStatus.TENTATIVE">
|
||||
<b-icon icon="calendar-question"></b-icon>
|
||||
<b-icon icon="calendar-question" />
|
||||
{{ $t('Tentative: Will be confirmed later') }}
|
||||
</b-radio-button>
|
||||
<b-radio-button v-model="event.status"
|
||||
name="status"
|
||||
type="is-success"
|
||||
:native-value="EventStatus.CONFIRMED">
|
||||
<b-icon icon="calendar-check"></b-icon>
|
||||
<b-icon icon="calendar-check" />
|
||||
{{ $t('Confirmed: Will happen') }}
|
||||
</b-radio-button>
|
||||
<b-radio-button v-model="event.status"
|
||||
name="status"
|
||||
type="is-danger"
|
||||
:native-value="EventStatus.CANCELLED">
|
||||
<b-icon icon="calendar-remove"></b-icon>
|
||||
<b-icon icon="calendar-remove" />
|
||||
{{ $t("Cancelled: Won't happen") }}
|
||||
</b-radio-button>
|
||||
</b-field>
|
||||
@ -191,7 +191,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</b-modal>
|
||||
<span ref="bottomObserver"></span>
|
||||
<span ref="bottomObserver" />
|
||||
<nav role="navigation" aria-label="main navigation" class="navbar" :class="{'is-fixed-bottom': showFixedNavbar }">
|
||||
<div class="container">
|
||||
<div class="navbar-menu">
|
||||
@ -395,6 +395,11 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('currentActor')
|
||||
setCurrentActor() {
|
||||
this.event.organizerActor = this.currentActor;
|
||||
}
|
||||
|
||||
private validateForm() {
|
||||
const form = this.$refs.form as HTMLFormElement;
|
||||
if (form.checkValidity()) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import {ParticipantRole} from "@/types/event.model";
|
||||
import {ParticipantRole} from "@/types/event.model";
|
||||
<template>
|
||||
<div class="container">
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<transition appear name="fade" mode="out-in">
|
||||
<div>
|
||||
<div class="header-picture" v-if="event.picture" :style="`background-image: url('${event.picture.url}')`" />
|
||||
@ -9,7 +11,7 @@
|
||||
<div class="title-and-participate-button">
|
||||
<div class="title-wrapper">
|
||||
<div class="date-component">
|
||||
<date-calendar-icon :date="event.beginsOn"></date-calendar-icon>
|
||||
<date-calendar-icon :date="event.beginsOn" />
|
||||
</div>
|
||||
<div class="title-and-informations">
|
||||
<h1 class="title">{{ event.title }}</h1>
|
||||
@ -49,7 +51,7 @@
|
||||
<template>
|
||||
<span>{{ $t('Event already passed')}}</span>
|
||||
</template>
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,6 +67,9 @@
|
||||
<b-tag type="is-info" v-if="event.visibility === EventVisibility.PUBLIC">{{ $t('Public event') }}</b-tag>
|
||||
<b-tag type="is-info" v-if="event.visibility === EventVisibility.UNLISTED">{{ $t('Private event') }}</b-tag>
|
||||
</span>
|
||||
<span v-if="!event.local">
|
||||
<b-tag type="is-primary">{{ event.organizerActor.domain }}</b-tag>
|
||||
</span>
|
||||
<router-link
|
||||
v-if="event.tags && event.tags.length > 0"
|
||||
v-for="tag in event.tags"
|
||||
@ -136,7 +141,7 @@
|
||||
</b-modal>
|
||||
</div>
|
||||
<span class="online-address" v-if="event.onlineAddress && urlToHostname(event.onlineAddress)">
|
||||
<b-icon icon="link"></b-icon>
|
||||
<b-icon icon="link" />
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@ -250,8 +255,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { EVENT_PERSON_PARTICIPATION, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import {
|
||||
EVENT_PERSON_PARTICIPATION,
|
||||
EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
|
||||
FETCH_EVENT,
|
||||
JOIN_EVENT,
|
||||
LEAVE_EVENT,
|
||||
} from '@/graphql/event';
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { EventModel, EventStatus, EventVisibility, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
@ -311,6 +322,15 @@ import 'intersection-observer';
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
subscribeToMore: {
|
||||
document: EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
|
||||
variables() {
|
||||
return {
|
||||
eventId: this.event.id,
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
},
|
||||
update: (data) => {
|
||||
if (data && data.person) return data.person.participations;
|
||||
return [];
|
||||
@ -341,6 +361,7 @@ export default class Event extends EventMixin {
|
||||
currentActor!: IPerson;
|
||||
identity: IPerson = new Person();
|
||||
participations: IParticipant[] = [];
|
||||
oldParticipationRole!: String;
|
||||
showMap: boolean = false;
|
||||
isReportModalActive: boolean = false;
|
||||
isJoinModalActive: boolean = false;
|
||||
@ -432,14 +453,10 @@ export default class Event extends EventMixin {
|
||||
reporterId: this.currentActor.id,
|
||||
reportedId: this.event.organizerActor.id,
|
||||
content,
|
||||
forward,
|
||||
},
|
||||
});
|
||||
this.$buefy.notification.open({
|
||||
message: this.$t('Event {eventTitle} reported', { eventTitle }) as string,
|
||||
type: 'is-success',
|
||||
position: 'is-bottom-right',
|
||||
duration: 5000,
|
||||
});
|
||||
this.$notifier.success(this.$t('Event {eventTitle} reported', { eventTitle }) as string);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -493,12 +510,11 @@ export default class Event extends EventMixin {
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.$buefy.notification.open({
|
||||
message: (data.joinEvent.role === ParticipantRole.NOT_APPROVED ? this.$t('Your participation has been requested') : this.$t('Your participation has been confirmed')) as string,
|
||||
type: 'is-success',
|
||||
position: 'is-bottom-right',
|
||||
duration: 5000,
|
||||
});
|
||||
if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
|
||||
this.participationRequestedMessage();
|
||||
} else {
|
||||
this.participationConfirmedMessage();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -563,18 +579,55 @@ export default class Event extends EventMixin {
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.$buefy.notification.open({
|
||||
message: this.$t('You have cancelled your participation') as string,
|
||||
type: 'is-success',
|
||||
position: 'is-bottom-right',
|
||||
duration: 5000,
|
||||
});
|
||||
this.participationCancelledMessage();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('participations')
|
||||
watchParticipations() {
|
||||
if (this.participations.length > 0) {
|
||||
if (this.oldParticipationRole
|
||||
&& this.participations[0].role !== ParticipantRole.NOT_APPROVED
|
||||
&& this.oldParticipationRole !== this.participations[0].role) {
|
||||
switch (this.participations[0].role) {
|
||||
case ParticipantRole.PARTICIPANT:
|
||||
this.participationConfirmedMessage();
|
||||
break;
|
||||
case ParticipantRole.REJECTED:
|
||||
this.participationRejectedMessage();
|
||||
break;
|
||||
default:
|
||||
this.participationChangedMessage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.oldParticipationRole = this.participations[0].role;
|
||||
}
|
||||
}
|
||||
|
||||
private participationConfirmedMessage() {
|
||||
this.$notifier.success(this.$t('Your participation has been confirmed') as string);
|
||||
}
|
||||
|
||||
private participationRequestedMessage() {
|
||||
this.$notifier.success(this.$t('Your participation has been requested') as string);
|
||||
}
|
||||
|
||||
private participationRejectedMessage() {
|
||||
this.$notifier.error(this.$t('Your participation has been rejected') as string);
|
||||
}
|
||||
|
||||
private participationChangedMessage() {
|
||||
this.$notifier.info(this.$t('Your participation status has been changed') as string);
|
||||
}
|
||||
|
||||
private participationCancelledMessage() {
|
||||
this.$notifier.success(this.$t('You have cancelled your participation') as string);
|
||||
}
|
||||
|
||||
async downloadIcsEvent() {
|
||||
const data = await (await fetch(`${GRAPHQL_API_ENDPOINT}/events/${this.uuid}/export/ics`)).text();
|
||||
const blob = new Blob([data], { type: 'text/calendar' });
|
||||
|
@ -64,7 +64,7 @@ export default class ReportList extends Vue {
|
||||
RouteName = RouteName;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.container li {
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
@ -27,7 +27,10 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('Reported by') }}</td>
|
||||
<td>
|
||||
<td v-if="report.reporter.type === ActorType.APPLICATION">
|
||||
{{ report.reporter.domain }}
|
||||
</td>
|
||||
<td v-else>
|
||||
<router-link :to="{ name: RouteName.PROFILE, params: { name: report.reporter.preferredUsername } }">
|
||||
<img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}
|
||||
</router-link>
|
||||
@ -55,15 +58,15 @@
|
||||
<td>
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid }}">{{ report.event.title }}</router-link>
|
||||
<span class="is-pulled-right">
|
||||
<b-button
|
||||
tag="router-link"
|
||||
type="is-primary"
|
||||
:to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"
|
||||
icon-left="pencil"
|
||||
size="is-small">{{ $t('Edit') }}</b-button>
|
||||
<!-- <b-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- type="is-primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmDelete()"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="is-small">{{ $t('Delete') }}</b-button>
|
||||
</span>
|
||||
@ -74,24 +77,24 @@
|
||||
</div>
|
||||
|
||||
<div class="box report-content">
|
||||
<p v-if="report.content" v-html="nl2br(report.content)"></p>
|
||||
<p v-if="report.content" v-html="nl2br(report.content)" />
|
||||
<p v-else>{{ $t('No comment') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="box" v-if="report.event && report.comments.length === 0">
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid }}">
|
||||
<h3 class="title">{{ report.event.title }}</h3>
|
||||
<p v-html="report.event.description"></p>
|
||||
<p v-html="report.event.description" />
|
||||
</router-link>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
type="is-primary"
|
||||
:to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"
|
||||
icon-left="pencil"
|
||||
size="is-small">{{ $t('Edit') }}</b-button>
|
||||
<!-- <b-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- type="is-primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmDelete()"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="is-small">{{ $t('Delete') }}</b-button>
|
||||
</div>
|
||||
@ -101,17 +104,25 @@
|
||||
<div class="box" v-if="comment">
|
||||
<article class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="comment.actor.avatar">
|
||||
<figure class="image is-48x48" v-if="comment.actor && comment.actor.avatar">
|
||||
<img :src="comment.actor.avatar.url" alt="Image">
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span v-if="comment.actor">
|
||||
<strong>{{ comment.actor.name }}</strong> <small>@{{ comment.actor.preferredUsername }}</small>
|
||||
</span>
|
||||
<span v-else>{{ $t('Unknown actor') }}</span>
|
||||
<br>
|
||||
<p v-html="comment.text"></p>
|
||||
<p v-html="comment.text" />
|
||||
</div>
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmCommentDelete(comment)"
|
||||
icon-left="delete"
|
||||
size="is-small">{{ $t('Delete') }}</b-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@ -131,21 +142,23 @@
|
||||
<b-field :label="$t('New note')">
|
||||
<b-input type="textarea" v-model="noteContent"></b-input>
|
||||
</b-field>
|
||||
<b-button type="submit" @click="addNote">{{ $t('Ajouter une note') }}</b-button>
|
||||
<b-button type="submit" @click="addNote">{{ $t('Add a note') }}</b-button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { CREATE_REPORT_NOTE, REPORT, REPORTS, UPDATE_REPORT } from '@/graphql/report';
|
||||
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from '@/graphql/report';
|
||||
import { IReport, IReportNote, ReportStatusEnum } from '@/types/report.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import { IPerson, ActorType } from '@/types/actor';
|
||||
import { DELETE_EVENT } from '@/graphql/event';
|
||||
import { uniq } from 'lodash';
|
||||
import { nl2br } from '@/utils/html';
|
||||
import { DELETE_COMMENT } from '@/graphql/comment';
|
||||
import { IComment } from '@/types/comment.model';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@ -164,6 +177,12 @@ import { nl2br } from '@/utils/html';
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('Report') as string,
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Report extends Vue {
|
||||
@Prop({ required: true }) reportId!: number;
|
||||
@ -173,6 +192,7 @@ export default class Report extends Vue {
|
||||
|
||||
ReportStatusEnum = ReportStatusEnum;
|
||||
RouteName = RouteName;
|
||||
ActorType = ActorType;
|
||||
nl2br = nl2br;
|
||||
|
||||
noteContent: string = '';
|
||||
@ -210,7 +230,7 @@ export default class Report extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
confirmDelete() {
|
||||
confirmEventDelete() {
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t('Deleting event') as string,
|
||||
message: this.$t('Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.') as string,
|
||||
@ -221,6 +241,17 @@ export default class Report extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
confirmCommentDelete(comment: IComment) {
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t('Deleting comment') as string,
|
||||
message: this.$t('Are you sure you want to <b>delete</b> this comment? This action cannot be undone.') as string,
|
||||
confirmText: this.$t('Delete Comment') as string,
|
||||
type: 'is-danger',
|
||||
hasIcon: true,
|
||||
onConfirm: () => this.deleteComment(comment),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteEvent() {
|
||||
if (!this.report.event || !this.report.event.id) return;
|
||||
const eventTitle = this.report.event.title;
|
||||
@ -245,6 +276,21 @@ export default class Report extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteComment(comment: IComment) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_COMMENT,
|
||||
variables: {
|
||||
commentId: comment.id,
|
||||
actorId: this.currentActor.id,
|
||||
},
|
||||
});
|
||||
this.$notifier.success(this.$t('Comment deleted') as string);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateReport(status: ReportStatusEnum) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
@ -289,10 +335,6 @@ export default class Report extends Vue {
|
||||
<style lang="scss" scoped>
|
||||
@import "@/variables.scss";
|
||||
|
||||
.container li {
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
tbody td img.image, .note img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
|
@ -9,15 +9,15 @@
|
||||
<b-field>
|
||||
<b-radio-button v-model="filterReports"
|
||||
:native-value="ReportStatusEnum.OPEN">
|
||||
Ouvert
|
||||
{{ $t('Open') }}
|
||||
</b-radio-button>
|
||||
<b-radio-button v-model="filterReports"
|
||||
:native-value="ReportStatusEnum.RESOLVED">
|
||||
Résolus
|
||||
{{ $t('Resolved') }}
|
||||
</b-radio-button>
|
||||
<b-radio-button v-model="filterReports"
|
||||
:native-value="ReportStatusEnum.CLOSED">
|
||||
Fermés
|
||||
{{ $t('Closed') }}
|
||||
</b-radio-button>
|
||||
</b-field>
|
||||
<ul v-if="reports.length > 0">
|
||||
@ -28,9 +28,9 @@
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.OPEN" type="is-info">No open reports yet</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">No resolved reports yet</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">No closed reports yet</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.OPEN" type="is-info">{{ $t('No open reports yet') }}</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">{{ $t('No resolved reports yet') }}</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">{{ $t('No closed reports yet') }}</b-message>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@ -80,8 +80,3 @@ export default class ReportList extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.container li {
|
||||
margin: 10px auto;
|
||||
}
|
||||
</style>
|
@ -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<boolean> | 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}<br>${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),
|
||||
});
|
||||
|
1186
js/yarn.lock
1186
js/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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
|
||||
with {:ok, %Follower{} = follower} <-
|
||||
follower
|
||||
|> Follower.changeset(attrs)
|
||||
|> Repo.update()
|
||||
|> 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
|
||||
|
@ -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
|
||||
|
@ -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 """
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
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
|
||||
|> Participant.changeset(attrs)
|
||||
|> Repo.update()
|
||||
} ->
|
||||
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))
|
||||
|
@ -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
|
||||
|
75
lib/mobilizon/share.ex
Normal file
75
lib/mobilizon/share.ex
Normal file
@ -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
|
@ -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()}
|
||||
|
@ -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"}
|
||||
|
@ -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, _} ->
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
||||
|
11
lib/mobilizon_web/cache/activity_pub.ex
vendored
11
lib/mobilizon_web/cache/activity_pub.ex
vendored
@ -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
|
||||
|
28
lib/mobilizon_web/channels/graphql_socket.ex
Normal file
28
lib/mobilizon_web/channels/graphql_socket.ex
Normal file
@ -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
|
@ -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}))
|
||||
|
@ -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
|
||||
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)
|
||||
|
@ -9,6 +9,7 @@ defmodule MobilizonWeb.WebFingerController do
|
||||
"""
|
||||
use MobilizonWeb, :controller
|
||||
|
||||
plug(MobilizonWeb.Plugs.Federating)
|
||||
alias Mobilizon.Service.WebFinger
|
||||
|
||||
@doc """
|
||||
|
@ -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.
|
||||
|
@ -22,8 +22,8 @@ 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
|
||||
@ -42,10 +42,16 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
|
||||
conn
|
||||
end
|
||||
|
||||
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
|
||||
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
|
||||
|
||||
_ ->
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
|
27
lib/mobilizon_web/plugs/federating.ex
Normal file
27
lib/mobilizon_web/plugs/federating.ex
Normal file
@ -0,0 +1,27 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# 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
|
79
lib/mobilizon_web/plugs/mapped_signature_to_identity.ex
Normal file
79
lib/mobilizon_web/plugs/mapped_signature_to_identity.ex
Normal file
@ -0,0 +1,79 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# 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
|
@ -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
|
||||
|
@ -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"}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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} ->
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
38
lib/mobilizon_web/schema/actors/application.ex
Normal file
38
lib/mobilizon_web/schema/actors/application.ex
Normal file
@ -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
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -35,7 +35,11 @@
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0;">
|
||||
<%= 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 %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
@ -59,10 +63,10 @@
|
||||
<%= if Map.has_key?(@report, :comments) && length(@report.comments) > 0 do %>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p><%= gettext "Comments" %></p>
|
||||
<h3><%= gettext "Comments" %></h3>
|
||||
<%= for comment <- @report.comments do %>
|
||||
<p style="margin: 0;">
|
||||
<%= comment.text %>
|
||||
<%= HtmlSanitizeEx.strip_tags(comment.text) %>
|
||||
</p>
|
||||
<% end %>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="100%" style="width: 100% !important;">
|
||||
|
@ -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) %>
|
||||
|
||||
|
@ -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) <>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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, "<meta name=server-injected-data>", 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, "<meta name=server-injected-data>", 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("<meta name=\"server-injected-data\" />", tags)
|
||||
|> String.replace("<meta name=server-injected-data>", tags)
|
||||
end
|
||||
end
|
||||
|
@ -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}
|
||||
)
|
||||
end
|
||||
|
||||
else
|
||||
{:ok, activity, participant}
|
||||
end
|
||||
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(),
|
||||
|
@ -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,36 +19,28 @@ 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
|
||||
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
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines the full audience based on mentions based on a private audience
|
||||
@ -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
|
||||
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
|
||||
def get_to_and_cc(_actor, mentions, :direct) do
|
||||
{mentions, []}
|
||||
end
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
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
|
||||
|
@ -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(
|
||||
{:ok, %Picture{id: picture_id}} <-
|
||||
object["attachment"]
|
||||
|> hd
|
||||
|> Map.get("url")
|
||||
|> hd
|
||||
|> Map.get("href")
|
||||
) do
|
||||
|> 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
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
],
|
||||
"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
|
||||
|
40
lib/service/activity_pub/converter/tombstone.ex
Normal file
40
lib/service/activity_pub/converter/tombstone.ex
Normal file
@ -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
|
@ -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" ->
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 = "<span class='h-card mention'>@<span>#{preferred_username}</span></span>"
|
||||
# %Actor{preferred_username: preferred_username} = actor ->
|
||||
# link = "<span class='h-card mention'>@<span>#{preferred_username}</span></span>"
|
||||
#
|
||||
# {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 =
|
||||
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{url}'>@<span>#{
|
||||
preferred_username
|
||||
}</span></a></span>"
|
||||
"<span class='h-card mention' data-user='#{id}'>@<span>#{preferred_username}</span></span>"
|
||||
|
||||
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}}
|
||||
|
||||
|
@ -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", [])
|
||||
|
@ -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}"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user