Merge branch 'feature/media-upload' into 'master'

Refactor media upload

See merge request framasoft/mobilizon!140
This commit is contained in:
Thomas Citharel 2019-05-24 17:38:33 +02:00
commit dfade9fb06
113 changed files with 4718 additions and 1328 deletions

View File

@ -100,7 +100,7 @@
#
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.FunctionArity, [max_arity: 9]},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},

2
.gitignore vendored
View File

@ -28,6 +28,8 @@ priv/data/*
!priv/data/.gitkeep
.vscode/
cover/
test/fixtures/image_tmp.jpg
test/uploads/
uploads/*
!uploads/.gitkeep
.idea

View File

@ -4,5 +4,5 @@ projects:
extensions:
endpoints:
dev:
url: 'http://localhost:4001/api'
url: 'http://localhost:4000/api'
introspect: true

View File

@ -1,7 +1,7 @@
FROM bitwalker/alpine-elixir:latest
RUN apk add inotify-tools postgresql-client yarn
RUN apk add --no-cache make gcc libc-dev argon2
RUN apk add --no-cache make gcc libc-dev argon2 imagemagick
RUN mix local.hex --force && mix local.rebar --force

View File

@ -14,7 +14,11 @@ config :mobilizon, :instance,
description: System.get_env("MOBILIZON_INSTANCE_DESCRIPTION") || "This is a Mobilizon instance",
version: "1.0.0-dev",
registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false,
repository: Mix.Project.config()[:source_url]
repository: Mix.Project.config()[:source_url],
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
banner_upload_limit: 4_000_000
config :mime, :types, %{
"application/activity+json" => ["activity-json"],
@ -31,6 +35,34 @@ config :mobilizon, MobilizonWeb.Endpoint,
email_from: "noreply@localhost",
email_to: "noreply@localhost"
# Upload configuration
config :mobilizon, MobilizonWeb.Upload,
uploader: MobilizonWeb.Uploaders.Local,
filters: [MobilizonWeb.Upload.Filter.Dedupe],
link_name: true,
proxy_remote: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :upload
]
]
config :mobilizon, MobilizonWeb.Uploaders.Local, uploads: "uploads"
config :mobilizon, :media_proxy,
enabled: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
@ -62,9 +94,6 @@ config :geolix,
}
]
config :arc,
storage: Arc.Storage.Local
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
config :mobilizon, Mobilizon.Service.Geospatial.Nominatim,

View File

@ -8,11 +8,10 @@ use Mix.Config
# with brunch.io to recompile .js and .css sources.
config :mobilizon, MobilizonWeb.Endpoint,
http: [
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4001
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000
],
url: [
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local",
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4001
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local"
],
debug_errors: true,
code_reloader: true,

View File

@ -1,28 +1,10 @@
use Mix.Config
# For production, we often load configuration from external
# sources, such as your system environment. For this reason,
# you won't find the :http configuration below, but set inside
# MobilizonWeb.Endpoint.init/2 when load_from_system_env is
# true. Any dynamic configuration should be done there.
#
# Don't forget to configure the url host to something meaningful,
# Phoenix uses this information when generating URLs.
#
# Finally, we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the mix phx.digest task
# which you typically run after static files are built.
config :mobilizon, MobilizonWeb.Endpoint,
load_from_system_env: true,
http: [:inet6, port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000],
url: [
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.me",
scheme: "https",
port: 443
],
http: [
ip: {127, 0, 0, 1},
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000
port: 80
],
secret_key_base:
System.get_env("MOBILIZON_SECRET") || "ThisShouldBeAVeryStrongStringPleaseReplaceMe",

View File

@ -33,6 +33,10 @@ config :mobilizon, Mobilizon.Repo,
config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.TestAdapter
config :mobilizon, MobilizonWeb.Upload, filters: [], link_name: false
config :mobilizon, MobilizonWeb.Uploaders.Local, uploads: "test/uploads"
config :exvcr,
vcr_cassette_library_dir: "test/fixtures/vcr_cassettes"

View File

@ -14,7 +14,7 @@
"dependencies": {
"apollo-absinthe-upload-link": "^1.5.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-client": "2.5.1",
"apollo-link": "^1.2.11",
"apollo-link-http": "^1.5.14",
"apollo-link-state": "^0.4.2",

View File

@ -91,6 +91,7 @@ export default class App extends Vue {
@import "~buefy/src/scss/components/form";
@import "~buefy/src/scss/components/modal";
@import "~buefy/src/scss/components/tag";
@import "~buefy/src/scss/components/upload";
@import "~buefy/src/scss/utils/_all";
.router-enter-active,

View File

@ -8,8 +8,8 @@
<li v-for="identity in identities" :key="identity.id">
<div class="media identity" v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }">
<div class="media-left">
<figure class="image is-48x48">
<img class="is-rounded" :src="identity.avatarUrl">
<figure class="image is-48x48" v-if="identity.avatar">
<img class="is-rounded" :src="identity.avatar.url">
</figure>
</div>

View File

@ -2,7 +2,7 @@
<router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }">
<div class="card-image" v-if="!event.image">
<figure class="image is-16by9">
<div class="tag-container">
<div class="tag-container" v-if="event.tags">
<b-tag v-for="tag in event.tags.slice(0, 3)" :key="tag.slug" type="is-secondary">{{ tag.title }}</b-tag>
</div>
<img src="https://picsum.photos/g/400/225/?random">

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-image" v-if="!group.bannerUrl">
<div class="card-image" v-if="!group.banner">
<figure class="image is-4by3">
<img src="https://picsum.photos/g/400/200/">
</figure>

View File

@ -40,8 +40,8 @@
v-if="currentUser.isLoggedIn && loggedPerson"
:to="{ name: 'MyAccount' }"
>
<figure class="image is-24x24">
<img :src="loggedPerson.avatarUrl">
<figure class="image is-24x24" v-if="loggedPerson.avatar">
<img :src="loggedPerson.avatar.url">
</figure>
<span>{{ loggedPerson.preferredUsername }}</span>
</router-link>

View File

@ -0,0 +1,29 @@
<template>
<b-field class="file">
<b-upload v-model="pictureFile" @input="onFileChanged">
<a class="button is-primary">
<b-icon icon="upload"></b-icon>
<span>Click to upload</span>
</a>
</b-upload>
<span class="file-name" v-if="pictureFile">
{{ pictureFile.name }}
</span>
</b-field>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { IPictureUpload } from '@/types/picture.model';
@Component
export default class PictureUpload extends Vue {
picture!: IPictureUpload;
pictureFile: File|null = null;
onFileChanged(file: File) {
this.picture = { file, name: file.name, alt: '' };
this.$emit('change', this.picture);
}
}
</script>

View File

@ -10,8 +10,12 @@ query($name:String!) {
summary,
preferredUsername,
suspended,
avatarUrl,
bannerUrl,
avatar {
url
},
banner {
url
},
feedTokens {
token
},
@ -28,7 +32,9 @@ export const LOGGED_PERSON = gql`
query {
loggedPerson {
id,
avatarUrl,
avatar {
url
},
preferredUsername,
}
}`;
@ -37,7 +43,9 @@ export const LOGGED_PERSON_WITH_GOING_TO_EVENTS = gql`
query {
loggedPerson {
id,
avatarUrl,
avatar {
url
},
preferredUsername,
goingToEvents {
uuid,
@ -56,7 +64,9 @@ query {
export const IDENTITIES = gql`
query {
identities {
avatarUrl,
avatar {
url
},
preferredUsername,
name
}
@ -72,7 +82,9 @@ mutation CreatePerson($preferredUsername: String!) {
preferredUsername,
name,
summary,
avatarUrl
avatar {
url
},
}
}
`;
@ -91,7 +103,9 @@ mutation ($preferredUsername: String!, $name: String!, $summary: String!, $email
preferredUsername,
name,
summary,
avatarUrl,
avatar {
url
},
}
}
`;
@ -106,8 +120,12 @@ query($name:String!) {
summary,
preferredUsername,
suspended,
avatarUrl,
bannerUrl,
avatar {
url
},
banner {
url
}
organizedEvents {
uuid,
title,

View File

@ -4,7 +4,9 @@ const participantQuery = `
role,
actor {
preferredUsername,
avatarUrl,
avatar {
url
},
name,
id
}
@ -24,8 +26,10 @@ export const FETCH_EVENT = gql`
endsOn,
status,
visibility,
thumbnail,
largeImage,
picture {
id
url
},
publishAt,
category,
# online_address,
@ -35,19 +39,23 @@ export const FETCH_EVENT = gql`
floor,
street,
locality,
postal_code,
postalCode,
region,
country,
geom
}
organizerActor {
avatarUrl,
avatar {
url
},
preferredUsername,
domain,
name,
},
# attributedTo {
# # avatarUrl,
# avatar {
# url,
# }
# preferredUsername,
# name,
# },
@ -66,7 +74,9 @@ export const FETCH_EVENT = gql`
description
},
organizerActor {
avatarUrl,
avatar {
url,
},
preferredUsername,
domain,
name,
@ -89,8 +99,10 @@ export const FETCH_EVENTS = gql`
endsOn,
status,
visibility,
thumbnail,
largeImage,
picture {
id
url
},
publishAt,
# online_address,
# phone_address,
@ -99,12 +111,16 @@ export const FETCH_EVENTS = gql`
locality
}
organizerActor {
avatarUrl,
avatar {
url
},
preferredUsername,
name,
},
attributedTo {
avatarUrl,
avatar {
url
},
preferredUsername,
name,
},
@ -124,20 +140,31 @@ export const CREATE_EVENT = gql`
mutation CreateEvent(
$title: String!,
$description: String!,
$organizerActorId: String!,
$organizerActorId: ID!,
$category: String!,
$beginsOn: DateTime!
$beginsOn: DateTime!,
$picture_file: Upload,
$picture_name: String,
) {
createEvent(
title: $title,
description: $description,
beginsOn: $beginsOn,
organizerActorId: $organizerActorId,
category: $category
category: $category,
picture: {
picture: {
file: $picture_file,
name: $picture_name,
}
}
) {
id,
uuid,
title
title,
picture {
url
}
}
}
`;

View File

@ -4,14 +4,16 @@ export const LOGGED_PERSON = gql`
query {
loggedPerson {
id,
avatarUrl,
avatar {
url
},
preferredUsername,
}
}`;
export const CREATE_FEED_TOKEN_ACTOR = gql`
mutation createFeedToken($actor_id: Int!) {
createFeedToken(actor_id: $actor_id) {
createFeedToken(actorId: $actor_id) {
token,
actor {
id

View File

@ -23,7 +23,9 @@ query SearchGroups($searchText: String!) {
searchGroups(search: $searchText) {
total,
elements {
avatarUrl,
avatar {
url
},
domain,
preferredUsername,
name,

View File

@ -1,3 +1,5 @@
import { IPicture } from '@/types/picture.model';
export interface IActor {
id?: string;
url: string;
@ -6,13 +8,13 @@ export interface IActor {
summary: string;
preferredUsername: string;
suspended: boolean;
avatarUrl: string;
bannerUrl: string;
avatar: IPicture | null;
banner: IPicture | null;
}
export class Actor implements IActor {
avatarUrl: string = '';
bannerUrl: string = '';
avatar: IPicture | null = null;
banner: IPicture | null = null;
domain: string | null = null;
name: string = '';
preferredUsername: string = '';

View File

@ -1,5 +1,7 @@
import { Actor, IActor } from './actor';
import { IAddress } from '@/types/address.model';
import { ITag } from '@/types/tag.model';
import { IAbstractPicture, IPicture } from '@/types/picture.model';
export enum EventStatus {
TENTATIVE,
@ -62,8 +64,7 @@ export interface IEvent {
joinOptions: EventJoinOptions;
thumbnail: string;
largeImage: string;
picture: IAbstractPicture|null;
organizerActor: IActor;
attributedTo: IActor;
@ -84,12 +85,10 @@ export class EventModel implements IEvent {
description: string = '';
endsOn: Date = new Date();
joinOptions: EventJoinOptions = EventJoinOptions.FREE;
largeImage: string = '';
local: boolean = true;
participants: IParticipant[] = [];
publishAt: Date = new Date();
status: EventStatus = EventStatus.CONFIRMED;
thumbnail: string = '';
title: string = '';
url: string = '';
uuid: string = '';
@ -99,4 +98,5 @@ export class EventModel implements IEvent {
relatedEvents: IEvent[] = [];
onlineAddress: string = '';
phoneAddress: string = '';
picture: IAbstractPicture|null = null;
}

View File

@ -0,0 +1,16 @@
export interface IAbstractPicture {
name;
alt;
}
export interface IPicture {
url;
name;
alt;
}
export interface IPictureUpload {
file: File;
name: String;
alt: String|null;
}

View File

@ -2,8 +2,8 @@
<section class="container">
<div v-if="person">
<div class="header">
<figure v-if="person.bannerUrl" class="image is-3by1">
<img :src="person.bannerUrl" alt="banner">
<figure v-if="person.banner" class="image is-3by1">
<img :src="person.banner.url" alt="banner">
</figure>
</div>

View File

@ -1,16 +1,16 @@
<template>
<section class="container">
<div v-if="person">
<div class="card-image" v-if="person.bannerUrl">
<div class="card-image" v-if="person.banner">
<figure class="image">
<img :src="person.bannerUrl">
<img :src="person.banner.url">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img :src="person.avatarUrl">
<figure class="image is-48x48" v-if="person.avatar">
<img :src="person.avatar.url">
</figure>
</div>
<div class="media-content">
@ -18,7 +18,6 @@
<p class="subtitle">@{{ person.preferredUsername }}</p>
</div>
</div>
<div class="content">
<vue-simple-markdown :source="person.summary"></vue-simple-markdown>
</div>
@ -58,7 +57,7 @@
</a>
</b-dropdown-item>
</b-dropdown>
<a class="button" v-else-if="loggedPerson" @click="createToken">
<a class="button" v-if="loggedPerson.id === person.id" @click="createToken">
<translate>Create token</translate>
</a>
</div>

View File

@ -81,7 +81,7 @@ export default class Register extends Vue {
@Prop({ type: String, required: true }) email!: string;
@Prop({ type: Boolean, required: false, default: false }) userAlreadyActivated!: boolean;
host: string = MOBILIZON_INSTANCE_HOST;
host?: string = MOBILIZON_INSTANCE_HOST;
person: IPerson = {
preferredUsername: '',
@ -90,8 +90,8 @@ export default class Register extends Vue {
id: '',
url: '',
suspended: false,
avatarUrl: '',
bannerUrl: '',
avatar: null,
banner: null,
domain: null,
feedTokens: [],
goingToEvents: [],

View File

@ -1,5 +1,5 @@
<template>
<section>
<section class="container">
<h1 class="title">
<translate>Create a new event</translate>
</h1>
@ -22,6 +22,8 @@
</b-select>
</b-field>
<picture-upload @change="handlePictureUploadChange" />
<button class="button is-primary">
<translate>Create my event</translate>
</button>
@ -41,8 +43,11 @@ import {
} from '@/types/event.model';
import { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor';
import PictureUpload from '@/components/PictureUpload.vue';
import { IPictureUpload } from '@/types/picture.model';
@Component({
components: { PictureUpload },
apollo: {
loggedPerson: {
query: LOGGED_PERSON,
@ -55,6 +60,8 @@ export default class CreateEvent extends Vue {
loggedPerson: IPerson = new Person();
categories: string[] = Object.keys(Category);
event: IEvent = new EventModel();
pictureFile?: File;
pictureName?: String;
createEvent(e: Event) {
e.preventDefault();
@ -62,15 +69,18 @@ export default class CreateEvent extends Vue {
this.event.attributedTo = this.loggedPerson;
if (this.event.uuid === '') {
console.log('event', this.event);
this.$apollo
.mutate({
mutation: CREATE_EVENT,
variables: {
title: this.event.title,
description: this.event.description,
beginsOn: this.event.beginsOn,
beginsOn: this.event.beginsOn.toISOString(),
category: this.event.category,
organizerActorId: this.event.organizerActor.id,
picture_file: this.pictureFile,
picture_name: this.pictureName,
},
})
.then(data => {
@ -100,6 +110,12 @@ export default class CreateEvent extends Vue {
}
}
handlePictureUploadChange(picture: IPictureUpload) {
console.log('picture upload change', picture);
this.pictureFile = picture.file;
this.pictureName = picture.name;
}
// getAddressData(addressData) {
// if (addressData !== null) {
// this.event.address = {

View File

@ -3,7 +3,10 @@
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="event">
<div class="header-picture container">
<figure class="image is-3by1">
<figure class="image is-3by1" v-if="event.picture">
<img :src="event.picture.url">
</figure>
<figure class="image is-3by1" v-else>
<img src="https://picsum.photos/600/200/">
</figure>
</div>
@ -95,10 +98,10 @@
<translate
:translate-params="{name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}"
v-if="event.organizerActor">By %{ name }</translate>
<figure v-if="event.organizerActor.avatarUrl" class="image is-48x48">
<figure v-if="event.organizerActor.avatar" class="image is-48x48">
<img
class="is-rounded"
:src="event.organizerActor.avatarUrl"
:src="event.organizerActor.avatar.url"
:alt="$gettextInterpolate('%{actor}\'s avatar', {actor: event.organizerActor.preferredUsername})" />
</figure>
</router-link>
@ -185,8 +188,8 @@
<!-- >-->
<!-- <div>-->
<!-- <figure>-->
<!-- <img v-if="!participant.actor.avatarUrl" src="https://picsum.photos/125/125/">-->
<!-- <img v-else :src="participant.actor.avatarUrl">-->
<!-- <img v-if="!participant.actor.avatar.url" src="https://picsum.photos/125/125/">-->
<!-- <img v-else :src="participant.actor.avatar.url">-->
<!-- </figure>-->
<!-- <span>{{ participant.actor.preferredUsername }}</span>-->
<!-- </div>-->

View File

@ -1,16 +1,16 @@
<template>
<section class="container">
<div v-if="group">
<div class="card-image" v-if="group.bannerUrl">
<div class="card-image" v-if="group.banner.url">
<figure class="image">
<img :src="group.bannerUrl">
<img :src="group.banner.url">
</figure>
</div>
<div class="box">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img :src="group.avatarUrl">
<img :src="group.avatar.url">
</figure>
</div>
<div class="media-content">

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ defmodule Mobilizon.Actors.Actor do
alias Mobilizon.Users.User
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Events.{Event, FeedToken}
alias Mobilizon.Media.File
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
@ -62,8 +63,6 @@ defmodule Mobilizon.Actors.Actor do
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private)
field(:suspended, :boolean, default: false)
field(:avatar_url, :string)
field(:banner_url, :string)
# field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
has_many(:followers, Follower, foreign_key: :target_actor_id)
has_many(:followings, Follower, foreign_key: :actor_id)
@ -71,6 +70,8 @@ defmodule Mobilizon.Actors.Actor do
many_to_many(:memberships, Actor, join_through: Member)
belongs_to(:user, User)
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
embeds_one(:avatar, File)
embeds_one(:banner, File)
timestamps()
end
@ -93,11 +94,11 @@ defmodule Mobilizon.Actors.Actor do
:keys,
:manually_approves_followers,
:suspended,
:avatar_url,
:banner_url,
:user_id
])
|> build_urls()
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> unique_username_validator()
|> validate_required([:preferred_username, :keys, :suspended, :url])
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
@ -119,10 +120,11 @@ defmodule Mobilizon.Actors.Actor do
:suspended,
:url,
:type,
:avatar_url,
:user_id
])
|> build_urls()
|> cast_embed(:avatar)
|> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator()
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
@ -152,9 +154,7 @@ defmodule Mobilizon.Actors.Actor do
:summary,
:preferred_username,
:keys,
:manually_approves_followers,
:avatar_url,
:banner_url
:manually_approves_followers
])
|> validate_required([
:url,
@ -165,6 +165,8 @@ defmodule Mobilizon.Actors.Actor do
:preferred_username,
:keys
])
|> cast_embed(:avatar)
|> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator()
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
@ -193,10 +195,10 @@ defmodule Mobilizon.Actors.Actor do
:name,
:domain,
:summary,
:preferred_username,
:avatar_url,
:banner_url
:preferred_username
])
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> build_urls(:Group)
|> put_change(:domain, nil)
|> put_change(:keys, Actors.create_keys())

View File

@ -11,7 +11,7 @@ defmodule Mobilizon.Actors do
alias Mobilizon.Actors.{Actor, Bot, Member, Follower}
alias Mobilizon.Service.ActivityPub
# import Exgravatar
require Logger
@doc false
def data() do
@ -57,9 +57,12 @@ defmodule Mobilizon.Actors do
end
# Get actor by ID and preload organized events, followers and followings
@spec get_actor_with_everything(integer()) :: Ecto.Query
@spec get_actor_with_everything(integer()) :: Ecto.Query.t()
defp do_get_actor_with_everything(id) do
from(a in Actor, where: a.id == ^id, preload: [:organized_events, :followers, :followings])
from(a in Actor,
where: a.id == ^id,
preload: [:organized_events, :followers, :followings]
)
end
@doc """
@ -239,24 +242,29 @@ defmodule Mobilizon.Actors do
"""
@spec insert_or_update_actor(map(), boolean()) :: {:ok, Actor.t()}
def insert_or_update_actor(data, preload \\ false) do
cs = Actor.remote_actor_creation(data)
cs =
data
|> Actor.remote_actor_creation()
{:ok, actor} =
Repo.insert(
cs,
on_conflict: [
set: [
keys: data.keys,
avatar_url: data.avatar_url,
banner_url: data.banner_url,
name: data.name,
summary: data.summary
]
],
conflict_target: [:url]
)
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
with {:ok, actor} <-
Repo.insert(
cs,
on_conflict: [
set: [
keys: data.keys,
name: data.name,
summary: data.summary
]
],
conflict_target: [:url]
) do
actor = if preload, do: Repo.preload(actor, [:followers]), else: actor
{:ok, actor}
else
err ->
Logger.error(inspect(err))
{:error, err}
end
end
# def increase_event_count(%Actor{} = actor) do
@ -291,7 +299,8 @@ defmodule Mobilizon.Actors do
{:error, :actor_not_found}
actor ->
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
actor = if preload, do: Repo.preload(actor, [:followers]), else: actor
{:ok, actor}
end
end
@ -371,7 +380,11 @@ defmodule Mobilizon.Actors do
"""
@spec get_local_actor_by_name(String.t()) :: Actor.t() | nil
def get_local_actor_by_name(name) do
Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
Repo.one(
from(a in Actor,
where: a.preferred_username == ^name and is_nil(a.domain)
)
)
end
@doc """
@ -435,6 +448,7 @@ defmodule Mobilizon.Actors do
{:ok, actor}
_ ->
Logger.error("Could not fetch by AP id")
{:error, "Could not fetch by AP id"}
end
end

View File

@ -6,6 +6,9 @@ defmodule Mobilizon.Application do
import Cachex.Spec
alias Mobilizon.Service.Export.{Feed, ICalendar}
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
@ -82,4 +85,13 @@ defmodule Mobilizon.Application do
MobilizonWeb.Endpoint.config_change(changed, removed)
:ok
end
def named_version, do: @name <> " " <> @version
def user_agent do
info =
"#{MobilizonWeb.Endpoint.url()} <#{Mobilizon.CommonConfig.get([:instance, :email], "")}>"
named_version() <> "; " <> info
end
end

View File

@ -22,4 +22,45 @@ defmodule Mobilizon.CommonConfig do
defp instance_config(), do: Application.get_env(:mobilizon, :instance)
defp to_bool(v), do: v == true or v == "true" or v == "True"
def get(key), do: get(key, nil)
def get([key], default), do: get(key, default)
def get([parent_key | keys], default) do
case :mobilizon
|> Application.get_env(parent_key)
|> get_in(keys) do
nil -> default
any -> any
end
end
def get(key, default) do
Application.get_env(:mobilizon, key, default)
end
def get!(key) do
value = get(key, nil)
if value == nil do
raise("Missing configuration value: #{inspect(key)}")
else
value
end
end
def put([key], value), do: put(key, value)
def put([parent_key | keys], value) do
parent =
Application.get_env(:mobilizon, parent_key)
|> put_in(keys, value)
Application.put_env(:mobilizon, parent_key, parent)
end
def put(key, value) do
Application.put_env(:mobilizon, key, value)
end
end

View File

@ -35,6 +35,7 @@ defmodule Mobilizon.Events.Event do
import Ecto.Changeset
alias Mobilizon.Events.{Event, Participant, Tag, Session, Track}
alias Mobilizon.Actors.Actor
alias Mobilizon.Media.Picture
alias Mobilizon.Addresses.Address
schema "events" do
@ -48,8 +49,6 @@ defmodule Mobilizon.Events.Event do
field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed)
field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public)
field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free)
field(:thumbnail, :string)
field(:large_image, :string)
field(:publish_at, :utc_datetime)
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
field(:online_address, :string)
@ -62,6 +61,7 @@ defmodule Mobilizon.Events.Event do
has_many(:tracks, Track)
has_many(:sessions, Session)
belongs_to(:physical_address, Address)
belongs_to(:picture, Picture)
timestamps(type: :utc_datetime)
end
@ -80,12 +80,11 @@ defmodule Mobilizon.Events.Event do
:category,
:status,
:visibility,
:thumbnail,
:large_image,
:publish_at,
:online_address,
:phone_address,
:uuid
:uuid,
:picture_id
])
|> cast_assoc(:tags)
|> cast_assoc(:physical_address)

View File

@ -178,7 +178,8 @@ defmodule Mobilizon.Events do
:tracks,
:tags,
:participants,
:physical_address
:physical_address,
:picture
]
)
|> Repo.one()
@ -692,7 +693,7 @@ defmodule Mobilizon.Events do
on: p.actor_id == a.id,
on: p.event_id == e.id,
where: a.id == ^id and p.role != ^:not_approved,
preload: [:tags]
preload: [:picture, :tags]
)
|> paginate(page, limit)
)
@ -1239,11 +1240,7 @@ defmodule Mobilizon.Events do
"""
def get_feed_token(token) do
from(
tk in FeedToken,
where: tk.token == ^token,
preload: [:actor, :user]
)
from(ftk in FeedToken, where: ftk.token == ^token, preload: [:actor, :user])
|> Repo.one()
end

115
lib/mobilizon/media.ex Normal file
View File

@ -0,0 +1,115 @@
defmodule Mobilizon.Media do
@moduledoc """
The Media context.
"""
import Ecto.Query, warn: false
alias Mobilizon.Repo
alias Mobilizon.Media.Picture
@doc false
def data() do
Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2)
end
@doc false
def query(queryable, _params) do
queryable
end
@doc """
Gets a single picture.
Raises `Ecto.NoResultsError` if the Picture does not exist.
## Examples
iex> get_picture!(123)
%Picture{}
iex> get_picture!(456)
** (Ecto.NoResultsError)
"""
def get_picture!(id), do: Repo.get!(Picture, id)
def get_picture(id), do: Repo.get(Picture, id)
@doc """
Get a picture by it's URL
"""
@spec get_picture_by_url(String.t()) :: Picture.t() | nil
def get_picture_by_url(url) do
from(
p in Picture,
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
)
|> Repo.one()
end
@doc """
Creates a picture.
## Examples
iex> create_picture(%{field: value})
{:ok, %Picture{}}
iex> create_picture(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_picture(attrs \\ %{}) do
%Picture{}
|> Picture.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a picture.
## Examples
iex> update_picture(picture, %{field: new_value})
{:ok, %Picture{}}
iex> update_picture(picture, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_picture(%Picture{} = picture, attrs) do
picture
|> Picture.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a Picture.
## Examples
iex> delete_picture(picture)
{:ok, %Picture{}}
iex> delete_picture(picture)
{:error, %Ecto.Changeset{}}
"""
def delete_picture(%Picture{} = picture) do
Repo.delete(picture)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking picture changes.
## Examples
iex> change_picture(picture)
%Ecto.Changeset{source: %Picture{}}
"""
def change_picture(%Picture{} = picture) do
Picture.changeset(picture, %{})
end
end

View File

@ -0,0 +1,22 @@
defmodule Mobilizon.Media.File do
@moduledoc """
Represents a file entity
"""
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field(:name, :string)
field(:url, :string)
field(:content_type, :string)
timestamps()
end
@doc false
def changeset(picture, attrs) do
picture
|> cast(attrs, [:name, :url, :content_type])
|> validate_required([:name, :url])
end
end

View File

@ -0,0 +1,21 @@
defmodule Mobilizon.Media.Picture do
@moduledoc """
Represents a picture entity
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Media.File
schema "pictures" do
embeds_one(:file, File, on_replace: :update)
timestamps()
end
@doc false
def changeset(picture, attrs) do
picture
|> cast(attrs, [])
|> cast_embed(:file)
end
end

121
lib/mobilizon/mime.ex Normal file
View File

@ -0,0 +1,121 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/mime.ex
defmodule Mobilizon.MIME do
@moduledoc """
Returns the mime-type of a binary and optionally a normalized file-name.
"""
@default "application/octet-stream"
@read_bytes 35
@spec file_mime_type(String.t()) ::
{:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error
def file_mime_type(path, filename) do
with {:ok, content_type} <- file_mime_type(path),
filename <- fix_extension(filename, content_type) do
{:ok, content_type, filename}
end
end
@spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error
def file_mime_type(filename) do
File.open(filename, [:read], fn f ->
check_mime_type(IO.binread(f, @read_bytes))
end)
end
def bin_mime_type(binary, filename) do
with {:ok, content_type} <- bin_mime_type(binary),
filename <- fix_extension(filename, content_type) do
{:ok, content_type, filename}
end
end
@spec bin_mime_type(binary()) :: {:ok, String.t()} | :error
def bin_mime_type(<<head::binary-size(@read_bytes), _::binary>>) do
{:ok, check_mime_type(head)}
end
def bin_mime_type(_), do: :error
def mime_type(<<_::binary>>), do: {:ok, @default}
defp fix_extension(filename, content_type) do
parts = String.split(filename, ".")
new_filename =
if length(parts) > 1 do
Enum.drop(parts, -1) |> Enum.join(".")
else
Enum.join(parts)
end
cond do
content_type == "application/octet-stream" ->
filename
ext = List.first(MIME.extensions(content_type)) ->
new_filename <> "." <> ext
true ->
Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".")
end
end
defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do
"image/png"
end
defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do
"image/gif"
end
defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do
"image/jpeg"
end
defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do
"video/webm"
end
defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do
"video/mp4"
end
defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do
"audio/mpeg"
end
defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do
"audio/mpeg"
end
defp check_mime_type(
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65,
0x6F, 0x72, 0x61, _::binary>>
) do
"video/ogg"
end
defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do
"audio/ogg"
end
defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do
"audio/wav"
end
defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do
"image/webp"
end
defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do
"video/avi"
end
defp check_mime_type(_) do
@default
end
end

View File

@ -26,8 +26,9 @@ defmodule MobilizonWeb.API.Events do
title <- String.trim(title),
mentions <- Formatter.parse_mentions(description),
visibility <- Map.get(args, :visibility, "public"),
{to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, visibility),
{to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, Atom.to_string(visibility)),
tags <- Formatter.parse_tags(description),
picture <- Map.get(args, :picture, nil),
content_html <-
make_content_html(
description,
@ -41,6 +42,7 @@ defmodule MobilizonWeb.API.Events do
to,
title,
content_html,
picture,
tags,
cc,
%{begins_on: begins_on},

View File

@ -5,11 +5,9 @@
defmodule MobilizonWeb.ActivityPubController do
use MobilizonWeb, :controller
alias Mobilizon.{Actors, Actors.Actor, Events}
alias Mobilizon.Events.{Event, Comment}
alias MobilizonWeb.ActivityPub.{ObjectView, ActorView}
alias Mobilizon.{Actors, Actors.Actor}
alias MobilizonWeb.ActivityPub.ActorView
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Service.Federator
require Logger

View File

@ -0,0 +1,45 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/media_proxy/controller.ex
defmodule MobilizonWeb.MediaProxyController do
use MobilizonWeb, :controller
alias MobilizonWeb.ReverseProxy
alias MobilizonWeb.MediaProxy
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
with config <- Mobilizon.CommonConfig.get([:media_proxy], []),
true <- Keyword.get(config, :enabled, false),
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
:ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
else
false ->
send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
{:error, :invalid_signature} ->
send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
{:wrong_filename, filename} ->
redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
end
end
def filename_matches(has_filename, path, url) do
filename =
url
|> MediaProxy.filename()
|> URI.decode()
path = URI.decode(path)
if has_filename && filename && Path.basename(path) != filename do
{:wrong_filename, filename}
else
:ok
end
end
end

View File

@ -4,9 +4,7 @@ defmodule MobilizonWeb.PageController do
"""
use MobilizonWeb, :controller
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment}
action_fallback(MobilizonWeb.FallbackController)

View File

@ -9,12 +9,7 @@ defmodule MobilizonWeb.Endpoint do
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug(
Plug.Static,
at: "/uploads",
from: "./uploads",
gzip: false
)
plug(MobilizonWeb.Plugs.UploadedMedia)
plug(
Plug.Static,
@ -38,7 +33,7 @@ defmodule MobilizonWeb.Endpoint do
plug(
Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
pass: ["*/*"],
json_decoder: Jason
)
@ -57,22 +52,4 @@ defmodule MobilizonWeb.Endpoint do
)
plug(MobilizonWeb.Router)
@doc """
Callback invoked for dynamically configuring the endpoint.
It receives the endpoint configuration and checks if
configuration should be loaded from the system environment.
"""
def init(_key, config) do
if config[:load_from_system_env] do
port =
System.get_env("MOBILIZON_INSTANCE_PORT") ||
raise "expected the MOBILIZON_INSTANCE_PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
else
{:ok, config}
end
end
end

View File

@ -0,0 +1,86 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/media_proxy/media_proxy.ex
defmodule MobilizonWeb.MediaProxy do
@moduledoc """
Handles proxifying media files
"""
@base64_opts [padding: false]
def url(nil), do: nil
def url(""), do: nil
def url("/" <> _ = url), do: url
def url(url) do
config = Application.get_env(:mobilizon, :media_proxy, [])
if !Keyword.get(config, :enabled, false) or
String.starts_with?(url, MobilizonWeb.Endpoint.url()) do
url
else
encode_url(url)
end
end
def encode_url(url) do
secret = Application.get_env(:mobilizon, MobilizonWeb.Endpoint)[:secret_key_base]
# Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
replacement = get_replacement(url, ":2F:")
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
url
|> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
|> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
build_url(sig64, base64, filename(url))
end
def decode_url(sig, url) do
secret = Application.get_env(:mobilizon, MobilizonWeb.Endpoint)[:secret_key_base]
sig = Base.url_decode64!(sig, @base64_opts)
local_sig = :crypto.hmac(:sha, secret, url)
if local_sig == sig do
{:ok, Base.url_decode64!(url, @base64_opts)}
else
{:error, :invalid_signature}
end
end
def filename(url_or_path) do
if path = URI.parse(url_or_path).path, do: Path.basename(path)
end
def build_url(sig_base64, url_base64, filename \\ nil) do
[
Mobilizon.CommonConfig.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()),
"proxy",
sig_base64,
url_base64,
filename
]
|> Enum.filter(fn value -> value end)
|> Path.join()
end
defp get_replacement(url, replacement) do
if String.contains?(url, replacement) do
get_replacement(url, replacement <> replacement)
else
replacement
end
end
end

View File

@ -0,0 +1,95 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/plugs/uploaded_media.ex
defmodule MobilizonWeb.Plugs.UploadedMedia do
@moduledoc """
Serves uploaded media files
"""
import Plug.Conn
require Logger
@behaviour Plug
# no slashes
@path "media"
def init(_opts) do
static_plug_opts =
[]
|> Keyword.put(:from, "__unconfigured_media_plug")
|> Keyword.put(:at, "/__unconfigured_media_plug")
|> Plug.Static.init()
%{static_plug_opts: static_plug_opts}
end
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
conn =
case fetch_query_params(conn) do
%{query_params: %{"name" => name}} = conn ->
name = String.replace(name, "\"", "\\\"")
conn
|> put_resp_header("content-disposition", "filename=\"#{name}\"")
conn ->
conn
end
config = Mobilizon.CommonConfig.get([MobilizonWeb.Upload])
with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false),
{:ok, get_method} <- uploader.get_file(file) do
get_media(conn, get_method, proxy_remote, opts)
else
_ ->
conn
|> send_resp(500, "Failed")
|> halt()
end
end
def call(conn, _opts), do: conn
defp get_media(conn, {:static_dir, directory}, _, opts) do
static_opts =
Map.get(opts, :static_plug_opts)
|> Map.put(:at, [@path])
|> Map.put(:from, directory)
conn = Plug.Static.call(conn, static_opts)
if conn.halted do
conn
else
conn
|> send_resp(404, "Not found")
|> halt()
end
end
defp get_media(conn, {:url, url}, true, _) do
conn
|> MobilizonWeb.ReverseProxy.call(
url,
Mobilizon.CommonConfig.get([Mobilizon.Upload, :proxy_opts], [])
)
end
defp get_media(conn, {:url, url}, _, _) do
conn
|> Phoenix.Controller.redirect(external: url)
|> halt()
end
defp get_media(conn, unknown, _, _) do
Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
conn
|> send_resp(500, "Internal Error")
|> halt()
end
end

View File

@ -5,6 +5,7 @@ defmodule MobilizonWeb.Resolvers.Event do
alias Mobilizon.Activity
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Media.Picture
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
@ -185,16 +186,27 @@ defmodule MobilizonWeb.Resolvers.Event do
@doc """
Create an event
"""
def create_event(_parent, args, %{context: %{current_user: _user}}) do
with {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <-
def create_event(_parent, args, %{context: %{current_user: _user}} = _resolution) do
with {:ok, args} <- save_attached_picture(args),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <-
MobilizonWeb.API.Events.create_event(args) do
{:ok,
%Event{
title: object["name"],
description: object["content"],
uuid: object["uuid"],
url: object["id"]
}}
res = %{
title: object["name"],
description: object["content"],
uuid: object["uuid"],
url: object["id"]
}
res =
if Map.has_key?(object, "attachment"),
do:
Map.put(res, :picture, %{
name: object["attachment"] |> hd() |> Map.get("name"),
url: object["attachment"] |> hd() |> Map.get("url") |> hd() |> Map.get("href")
}),
else: res
{:ok, res}
end
end
@ -202,6 +214,22 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to create events"}
end
# If we have an attached picture, just transmit it. It will be handled by
# Mobilizon.Service.ActivityPub.Utils.make_picture_data/1
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(%{picture: %{picture: %Plug.Upload{} = _picture}} = args), do: args
# Otherwise if we use a previously uploaded picture we need to fetch it from database
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do
with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
{:ok, Map.put(args, :picture, picture)}
end
end
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(args), do: {:ok, args}
@doc """
Delete an event
"""

View File

@ -47,12 +47,17 @@ defmodule MobilizonWeb.Resolvers.Person do
@doc """
This function is used to create more identities from an existing user
"""
def create_person(_parent, %{preferred_username: _preferred_username} = args, %{
context: %{current_user: user}
}) do
def create_person(
_parent,
%{preferred_username: _preferred_username} = args,
%{
context: %{current_user: user}
} = _resolution
) do
args = Map.put(args, :user_id, user.id)
with {:ok, %Actor{} = new_person} <- Actors.new_person(args) do
with args <- save_attached_pictures(args),
{:ok, %Actor{} = new_person} <- Actors.new_person(args) do
{:ok, new_person}
end
end
@ -64,6 +69,21 @@ defmodule MobilizonWeb.Resolvers.Person do
{:error, "You need to be logged-in to create a new identity"}
end
defp save_attached_pictures(args) do
Enum.reduce([:avatar, :banner], args, fn key, args ->
if Map.has_key?(args, key) do
pic = args[key][:picture]
with {:ok, %{"name" => name, "url" => [%{"href" => url, "mediaType" => content_type}]}} <-
MobilizonWeb.Upload.store(pic.file, type: key, description: pic.alt) do
Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type})
end
else
args
end
end)
end
@doc """
This function is used to register a person afterwards the user has been created (but not activated)
"""
@ -71,6 +91,7 @@ defmodule MobilizonWeb.Resolvers.Person do
with {:ok, %User{} = user} <- Users.get_user_by_email(args.email),
{:no_actor, nil} <- {:no_actor, Users.get_actor_for_user(user)},
args <- Map.put(args, :user_id, user.id),
args <- save_attached_pictures(args),
{:ok, %Actor{} = new_person} <- Actors.new_person(args) do
{:ok, new_person}
else

View File

@ -0,0 +1,64 @@
defmodule MobilizonWeb.Resolvers.Picture do
@moduledoc """
Handles the picture-related GraphQL calls
"""
alias Mobilizon.Media
alias Mobilizon.Media.Picture
@doc """
Get picture for an event's pic
"""
def picture(%{picture_id: picture_id} = _parent, _args, _resolution) do
with {:ok, picture} <- do_fetch_picture(picture_id) do
{:ok, picture}
end
end
@doc """
Get picture for an event that has an attached
See MobilizonWeb.Resolvers.Event.create_event/3
"""
def picture(%{picture: picture} = _parent, _args, _resolution) do
{:ok, picture}
end
def picture(_parent, %{id: picture_id}, _resolution), do: do_fetch_picture(picture_id)
def picture(_parent, _args, _resolution) do
{:ok, nil}
end
@spec do_fetch_picture(nil) :: {:error, nil}
defp do_fetch_picture(nil), do: {:error, nil}
@spec do_fetch_picture(String.t()) :: {:ok, Picture.t()} | {:error, :not_found}
defp do_fetch_picture(picture_id) do
with %Picture{id: id, file: file} = _pic <- Media.get_picture(picture_id) do
{:ok, %{name: file.name, url: file.url, id: id}}
else
_err ->
{:error, "Picture with ID #{picture_id} was not found"}
end
end
@spec upload_picture(map(), map(), map()) :: {:ok, Picture.t()} | {:error, any()}
def upload_picture(_parent, %{file: %Plug.Upload{} = file} = args, %{
context: %{
current_user: _user
}
}) do
with {:ok, %{"url" => [%{"href" => url}]}} <- MobilizonWeb.Upload.store(file),
args <- Map.put(args, :url, url),
{:ok, picture = %Picture{}} <- Media.create_picture(%{"file" => args}) do
{:ok, %{name: picture.file.name, url: picture.file.url, id: picture.id}}
else
err ->
{:error, err}
end
end
def upload_picture(_parent, _args, _resolution) do
{:error, "You need to login to upload a picture"}
end
end

View File

@ -0,0 +1,382 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/reverse_proxy.ex
defmodule MobilizonWeb.ReverseProxy do
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
~w(if-unmodified-since if-none-match if-range range)
@resp_cache_headers ~w(etag date last-modified cache-control)
@keep_resp_headers @resp_cache_headers ++
~w(content-type content-disposition content-encoding content-range) ++
~w(accept-ranges vary)
@default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30)
@max_body_length :infinity
@methods ~w(GET HEAD)
@moduledoc """
A reverse proxy.
MobilizonWeb.ReverseProxy.call(conn, url, options)
It is not meant to be added into a plug pipeline, but to be called from another plug or controller.
Supports `#{inspect(@methods)}` HTTP methods, and only allows `#{inspect(@valid_resp_codes)}` status codes.
Responses are chunked to the client while downloading from the upstream.
Some request / responses headers are preserved:
* request: `#{inspect(@keep_req_headers)}`
* response: `#{inspect(@keep_resp_headers)}`
If no caching headers (`#{inspect(@resp_cache_headers)}`) are returned by upstream, `cache-control` will be
set to `#{inspect(@default_cache_control_header)}`.
Options:
* `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP
errors. Any error during body processing will not be redirected as the response is chunked. This may expose
remote URL, clients IPs, .
* `max_body_length` (default `#{inspect(@max_body_length)}`): limits the content length to be approximately the
specified length. It is validated with the `content-length` header and also verified when proxying.
* `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to
read from the remote upstream.
* `inline_content_types`:
* `true` will not alter `content-disposition` (up to the upstream),
* `false` will add `content-disposition: attachment` to any request,
* a list of whitelisted content types
* `keep_user_agent` will forward the client's user-agent to the upstream. This may be useful if the upstream is
doing content transformation (encoding, ) depending on the request.
* `req_headers`, `resp_headers` additional headers.
* `http`: options for [hackney](https://github.com/benoitc/hackney).
"""
@hackney Application.get_env(:mobilizon, :hackney, :hackney)
@httpoison Application.get_env(:mobilizon, :httpoison, HTTPoison)
@default_hackney_options []
@inline_content_types [
"image/gif",
"image/jpeg",
"image/jpg",
"image/png",
"image/svg+xml",
"audio/mpeg",
"audio/mp3",
"video/webm",
"video/mp4",
"video/quicktime"
]
require Logger
import Plug.Conn
@type option() ::
{:keep_user_agent, boolean}
| {:max_read_duration, :timer.time() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity}
| {:http, []}
| {:req_headers, [{String.t(), String.t()}]}
| {:resp_headers, [{String.t(), String.t()}]}
| {:inline_content_types, boolean() | [String.t()]}
| {:redirect_on_failure, boolean()}
@spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t()
def call(_conn, _url, _opts \\ [])
def call(conn = %{method: method}, url, opts) when method in @methods do
hackney_opts =
@default_hackney_options
|> Keyword.merge(Keyword.get(opts, :http, []))
|> @httpoison.process_request_options()
req_headers = build_req_headers(conn.req_headers, opts)
opts =
if filename = MobilizonWeb.MediaProxy.filename(url) do
Keyword.put_new(opts, :attachment_name, filename)
else
opts
end
with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
:ok <- header_length_constraint(headers, Keyword.get(opts, :max_body_length)) do
response(conn, client, url, code, headers, opts)
else
{:ok, code, headers} ->
head_response(conn, url, code, headers, opts)
|> halt()
{:error, {:invalid_http_response, code}} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
conn
|> error_or_redirect(
url,
code,
"Request failed: " <> Plug.Conn.Status.reason_phrase(code),
opts
)
|> halt()
{:error, error} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
conn
|> error_or_redirect(url, 500, "Request failed", opts)
|> halt()
end
end
def call(conn, _, _) do
conn
|> send_resp(400, Plug.Conn.Status.reason_phrase(400))
|> halt()
end
defp request(method, url, headers, hackney_opts) do
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom()
case @hackney.request(method, url, headers, "", hackney_opts) do
{:ok, code, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client}
{:ok, code, headers} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers)}
{:ok, code, _, _} ->
{:error, {:invalid_http_response, code}}
{:error, error} ->
{:error, error}
end
end
defp response(conn, client, url, status, headers, opts) do
result =
conn
|> put_resp_headers(build_resp_headers(headers, opts))
|> send_chunked(status)
|> chunk_reply(client, opts)
case result do
{:ok, conn} ->
halt(conn)
{:error, :closed, conn} ->
:hackney.close(client)
halt(conn)
{:error, error, conn} ->
Logger.warn(
"#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
)
:hackney.close(client)
halt(conn)
end
end
defp chunk_reply(conn, client, opts) do
chunk_reply(conn, client, opts, 0, 0)
end
defp chunk_reply(conn, client, opts, sent_so_far, duration) do
with {:ok, duration} <-
check_read_duration(
duration,
Keyword.get(opts, :max_read_duration, @max_read_duration)
),
{:ok, data} <- @hackney.stream_body(client),
{:ok, duration} <- increase_read_duration(duration),
sent_so_far = sent_so_far + byte_size(data),
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
{:ok, conn} <- chunk(conn, data) do
chunk_reply(conn, client, opts, sent_so_far, duration)
else
:done -> {:ok, conn}
{:error, error} -> {:error, error, conn}
end
end
defp head_response(conn, _url, code, headers, opts) do
conn
|> put_resp_headers(build_resp_headers(headers, opts))
|> send_resp(code, "")
end
defp error_or_redirect(conn, url, code, body, opts) do
if Keyword.get(opts, :redirect_on_failure, false) do
conn
|> Phoenix.Controller.redirect(external: url)
|> halt()
else
conn
|> send_resp(code, body)
|> halt
end
end
defp downcase_headers(headers) do
Enum.map(headers, fn {k, v} ->
{String.downcase(k), v}
end)
end
defp get_content_type(headers) do
{_, content_type} =
List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"})
[content_type | _] = String.split(content_type, ";")
content_type
end
defp put_resp_headers(conn, headers) do
Enum.reduce(headers, conn, fn {k, v}, conn ->
put_resp_header(conn, k, v)
end)
end
defp build_req_headers(headers, opts) do
headers
|> downcase_headers()
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
|> (fn headers ->
headers = headers ++ Keyword.get(opts, :req_headers, [])
if Keyword.get(opts, :keep_user_agent, false) do
List.keystore(
headers,
"user-agent",
0,
{"user-agent", Mobilizon.Application.user_agent()}
)
else
headers
end
end).()
end
defp build_resp_headers(headers, opts) do
headers
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts)
|> build_resp_content_disposition_header(opts)
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
end
defp build_resp_cache_headers(headers, _opts) do
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
has_cache_control? = List.keymember?(headers, "cache-control", 0)
cond do
has_cache? && has_cache_control? ->
headers
has_cache? ->
# There's caching header present but no cache-control -- we need to explicitely override it
# to public as Plug defaults to "max-age=0, private, must-revalidate"
List.keystore(headers, "cache-control", 0, {"cache-control", "public"})
true ->
List.keystore(
headers,
"cache-control",
0,
{"cache-control", @default_cache_control_header}
)
end
end
defp build_resp_content_disposition_header(headers, opts) do
opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
content_type = get_content_type(headers)
attachment? =
cond do
is_list(opt) && !Enum.member?(opt, content_type) -> true
opt == false -> true
true -> false
end
if attachment? do
name =
try do
{{"content-disposition", content_disposition_string}, _} =
List.keytake(headers, "content-disposition", 0)
[name | _] =
Regex.run(
~r/filename="((?:[^"\\]|\\.)*)"/u,
content_disposition_string || "",
capture: :all_but_first
)
name
rescue
MatchError -> Keyword.get(opts, :attachment_name, "attachment")
end
disposition = "attachment; filename=\"#{name}\""
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
else
headers
end
end
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
with {_, size} <- List.keyfind(headers, "content-length", 0),
{size, _} <- Integer.parse(size),
true <- size <= limit do
:ok
else
false ->
{:error, :body_too_large}
_ ->
:ok
end
end
defp header_length_constraint(_, _), do: :ok
defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do
{:error, :body_too_large}
end
defp body_size_constraint(_, _), do: :ok
defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do
if duration > max do
{:error, :read_duration_exceeded}
else
{:ok, {duration, :erlang.system_time(:millisecond)}}
end
end
defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit}
defp increase_read_duration({previous_duration, started})
when is_integer(previous_duration) and is_integer(started) do
duration = :erlang.system_time(:millisecond) - started
{:ok, previous_duration + duration}
end
defp increase_read_duration(_) do
{:ok, :no_duration_limit, :no_duration_limit}
end
end

View File

@ -5,7 +5,7 @@ defmodule MobilizonWeb.Router do
use MobilizonWeb, :router
pipeline :graphql do
plug(:accepts, ["json"])
# plug(:accepts, ["json"])
plug(MobilizonWeb.AuthPipeline)
end
@ -102,7 +102,6 @@ defmodule MobilizonWeb.Router do
scope "/", MobilizonWeb do
pipe_through(:browser)
forward("/uploads", UploadPlug)
get("/*path", PageController, :index)
end
end

View File

@ -4,7 +4,7 @@ defmodule MobilizonWeb.Schema do
"""
use Absinthe.Schema
alias Mobilizon.{Actors, Events, Users, Addresses}
alias Mobilizon.{Actors, Events, Users, Addresses, Media}
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Events.{Event, Comment, Participant}
@ -14,6 +14,7 @@ defmodule MobilizonWeb.Schema do
import_types(Absinthe.Plug.Types)
import_types(MobilizonWeb.Schema.UserType)
import_types(MobilizonWeb.Schema.PictureType)
import_types(MobilizonWeb.Schema.ActorInterface)
import_types(MobilizonWeb.Schema.Actors.PersonType)
import_types(MobilizonWeb.Schema.Actors.GroupType)
@ -32,12 +33,6 @@ defmodule MobilizonWeb.Schema do
field(:user, non_null(:user), description: "The user associated to this session")
end
@desc "A picture"
object :picture do
field(:url, :string, description: "The URL for this picture")
field(:url_thumbnail, :string, description: "The URL for this picture's thumbnail")
end
@desc """
Represents a notification for an user
"""
@ -91,6 +86,7 @@ defmodule MobilizonWeb.Schema do
|> Dataloader.add_source(Users, Users.data())
|> Dataloader.add_source(Events, Events.data())
|> Dataloader.add_source(Addresses, Addresses.data())
|> Dataloader.add_source(Media, Media.data())
Map.put(ctx, :loader, loader)
end
@ -112,6 +108,7 @@ defmodule MobilizonWeb.Schema do
import_fields(:tag_queries)
import_fields(:address_queries)
import_fields(:config_queries)
import_fields(:picture_queries)
end
@desc """
@ -126,11 +123,6 @@ defmodule MobilizonWeb.Schema do
import_fields(:participant_mutations)
import_fields(:member_mutations)
import_fields(:feed_token_mutations)
# @desc "Upload a picture"
# field :upload_picture, :picture do
# arg(:file, non_null(:upload))
# resolve(&Resolvers.Upload.upload_picture/3)
# end
import_fields(:picture_mutations)
end
end

View File

@ -5,10 +5,11 @@ defmodule MobilizonWeb.Schema.ActorInterface do
use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
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
@ -27,8 +28,9 @@ defmodule MobilizonWeb.Schema.ActorInterface do
)
field(:suspended, :boolean, description: "If the actor is suspended")
field(:avatar_url, :string, description: "The actor's avatar url")
field(:banner_url, :string, description: "The actor's banner url")
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")

View File

@ -5,7 +5,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
import_types(MobilizonWeb.Schema.Actors.MemberType)
alias MobilizonWeb.Resolvers
alias MobilizonWeb.Resolvers.{Member, Group}
alias Mobilizon.Events
@desc """
@ -29,8 +29,9 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
)
field(:suspended, :boolean, description: "If the actor is suspended")
field(:avatar_url, :string, description: "The actor's avatar url")
field(:banner_url, :string, description: "The actor's banner url")
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")
@ -51,7 +52,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
)
field(:members, non_null(list_of(:member)),
resolve: &Resolvers.Member.find_members_for_group/3,
resolve: &Member.find_members_for_group/3,
description: "List of group members"
)
end
@ -80,13 +81,13 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
field :groups, list_of(:group) do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Resolvers.Group.list_groups/3)
resolve(&Group.list_groups/3)
end
@desc "Get a group by it's preferred username"
field :group, :group do
arg(:preferred_username, non_null(:string))
resolve(&Resolvers.Group.find_group/3)
resolve(&Group.find_group/3)
end
end
@ -101,7 +102,17 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
description: "The actor's username which will be the admin (otherwise user's default one)"
)
resolve(&Resolvers.Group.create_group/3)
arg(:avatar, :picture_input,
description:
"The avatar for the group, either as an object or directly the ID of an existing Picture"
)
arg(:banner, :picture_input,
description:
"The banner for the group, either as an object or directly the ID of an existing Picture"
)
resolve(&Group.create_group/3)
end
@desc "Delete a group"
@ -109,7 +120,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
arg(:group_id, non_null(:integer))
arg(:actor_id, non_null(:integer))
resolve(&Resolvers.Group.delete_group/3)
resolve(&Group.delete_group/3)
end
end
end

View File

@ -5,7 +5,7 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
alias Mobilizon.Events
alias MobilizonWeb.Resolvers
alias MobilizonWeb.Resolvers.Person
import MobilizonWeb.Schema.Utils
import_types(MobilizonWeb.Schema.Events.FeedTokenType)
@ -34,8 +34,9 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
)
field(:suspended, :boolean, description: "If the actor is suspended")
field(:avatar_url, :string, description: "The actor's avatar url")
field(:banner_url, :string, description: "The actor's banner url")
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")
@ -56,25 +57,25 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
@desc "The list of events this person goes to"
field :going_to_events, list_of(:event) do
resolve(&Resolvers.Person.person_going_to_events/3)
resolve(&Person.person_going_to_events/3)
end
end
object :person_queries do
@desc "Get the current actor for the logged-in user"
field :logged_person, :person do
resolve(&Resolvers.Person.get_current_person/3)
resolve(&Person.get_current_person/3)
end
@desc "Get a person by it's preferred username"
field :person, :person do
arg(:preferred_username, non_null(:string))
resolve(&Resolvers.Person.find_person/3)
resolve(&Person.find_person/3)
end
@desc "Get the persons for an user"
field :identities, list_of(:person) do
resolve(&Resolvers.Person.identities/3)
resolve(&Person.identities/3)
end
end
@ -87,7 +88,17 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
arg(:summary, :string, description: "The summary for the new profile", default_value: "")
resolve(handle_errors(&Resolvers.Person.create_person/3))
arg(:avatar, :picture_input,
description:
"The avatar for the profile, either as an object or directly the ID of an existing Picture"
)
arg(:banner, :picture_input,
description:
"The banner for the profile, either as an object or directly the ID of an existing Picture"
)
resolve(handle_errors(&Person.create_person/3))
end
@desc "Register a first profile on registration"
@ -99,7 +110,17 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
arg(:summary, :string, description: "The summary for the new profile", default_value: "")
arg(:email, non_null(:string), description: "The email from the user previously created")
resolve(handle_errors(&Resolvers.Person.register_person/3))
arg(:avatar, :picture_input,
description:
"The avatar for the profile, either as an object or directly the ID of an existing Picture"
)
arg(:banner, :picture_input,
description:
"The banner for the profile, either as an object or directly the ID of an existing Picture"
)
resolve(handle_errors(&Person.register_person/3))
end
end
end

View File

@ -8,7 +8,7 @@ defmodule MobilizonWeb.Schema.EventType do
import_types(MobilizonWeb.Schema.AddressType)
import_types(MobilizonWeb.Schema.Events.ParticipantType)
import_types(MobilizonWeb.Schema.TagType)
alias MobilizonWeb.Resolvers
alias MobilizonWeb.Resolvers.{Picture, Event, Tag}
@desc "An event"
object :event do
@ -23,10 +23,12 @@ defmodule MobilizonWeb.Schema.EventType do
field(:ends_on, :datetime, description: "Datetime for when the event ends")
field(:status, :event_status, description: "Status of the event")
field(:visibility, :event_visibility, description: "The event's visibility")
# TODO replace me with picture object
field(:thumbnail, :string, description: "A thumbnail picture for the event")
# TODO replace me with banner
field(:large_image, :string, description: "A large picture for the event")
field(:picture, :picture,
description: "The event's picture",
resolve: &Picture.picture/3
)
field(:publish_at, :datetime, description: "When the event was published")
field(:physical_address, :address,
@ -45,19 +47,19 @@ defmodule MobilizonWeb.Schema.EventType do
field(:attributed_to, :actor, description: "Who the event is attributed to (often a group)")
field(:tags, list_of(:tag),
resolve: &MobilizonWeb.Resolvers.Tag.list_tags_for_event/3,
resolve: &Tag.list_tags_for_event/3,
description: "The event's tags"
)
field(:category, :string, description: "The event's category")
field(:participants, list_of(:participant),
resolve: &MobilizonWeb.Resolvers.Event.list_participants_for_event/3,
resolve: &Event.list_participants_for_event/3,
description: "The event's participants"
)
field(:related_events, list_of(:event),
resolve: &MobilizonWeb.Resolvers.Event.list_related_events/3,
resolve: &Event.list_related_events/3,
description: "Events related to this one"
)
@ -93,13 +95,13 @@ defmodule MobilizonWeb.Schema.EventType do
field :events, list_of(:event) do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Resolvers.Event.list_events/3)
resolve(&Event.list_events/3)
end
@desc "Get an event by uuid"
field :event, :event do
arg(:uuid, non_null(:uuid))
resolve(&Resolvers.Event.find_event/3)
resolve(&Event.find_event/3)
end
end
@ -113,15 +115,20 @@ defmodule MobilizonWeb.Schema.EventType do
arg(:state, :integer)
arg(:status, :integer)
arg(:public, :boolean)
arg(:thumbnail, :string)
arg(:large_image, :string)
arg(:visibility, :event_visibility, default_value: :private)
arg(:picture, :picture_input,
description:
"The picture for the event, either as an object or directly the ID of an existing Picture"
)
arg(:publish_at, :datetime)
arg(:online_address, :string)
arg(:phone_address, :string)
arg(:organizer_actor_id, non_null(:id))
arg(:category, non_null(:string))
resolve(&Resolvers.Event.create_event/3)
resolve(&Event.create_event/3)
end
@desc "Delete an event"
@ -129,7 +136,7 @@ defmodule MobilizonWeb.Schema.EventType do
arg(:event_id, non_null(:integer))
arg(:actor_id, non_null(:integer))
resolve(&Resolvers.Event.delete_event/3)
resolve(&Event.delete_event/3)
end
end
end

View File

@ -0,0 +1,48 @@
defmodule MobilizonWeb.Schema.PictureType do
@moduledoc """
Schema representation for Pictures
"""
use Absinthe.Schema.Notation
alias MobilizonWeb.Resolvers.Picture
@desc "A picture"
object :picture do
field(:id, :id, description: "The picture's ID")
field(:alt, :string, description: "The picture's alternative text")
field(:name, :string, description: "The picture's name")
field(:url, :string, description: "The picture's full URL")
end
@desc "An attached picture or a link to a picture"
input_object :picture_input do
# Either a full picture object
field(:picture, :picture_input_object)
# Or directly the ID of an existing picture
field(:picture_id, :string)
end
@desc "An attached picture"
input_object :picture_input_object do
field(:name, non_null(:string))
field(:alt, :string)
field(:file, non_null(:upload))
end
object :picture_queries do
@desc "Get a picture"
field :picture, :picture do
arg(:id, non_null(:string))
resolve(&Picture.picture/3)
end
end
object :picture_mutations do
@desc "Upload a picture"
field :upload_picture, :picture do
arg(:name, non_null(:string))
arg(:alt, :string)
arg(:file, non_null(:upload))
resolve(&Picture.upload_picture/3)
end
end
end

160
lib/mobilizon_web/upload.ex Normal file
View File

@ -0,0 +1,160 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/upload.ex
defmodule MobilizonWeb.Upload do
@moduledoc """
Manage user uploads
Options:
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration
* `:description`: upload alternative text
* `:base_url`: override base url
* `:uploader`: override uploader
* `:filters`: override filters
* `:size_limit`: override size limit
* `:activity_type`: override activity type
The `%MobilizonWeb.Upload{}` struct: all documented fields are meant to be overwritten in filters:
* `:id` - the upload id.
* `:name` - the upload file name.
* `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
is once created permanent and changing it (especially in uploaders) is probably a bad idea!
* `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
Related behaviors:
* `MobilizonWeb.Uploaders.Uploader`
* `MobilizonWeb.Upload.Filter`
"""
alias Ecto.UUID
require Logger
@type source ::
Plug.Upload.t()
| (data_uri_string :: String.t())
| {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
@type option ::
{:type, :avatar | :banner | :background}
| {:description, String.t()}
| {:activity_type, String.t()}
| {:size_limit, nil | non_neg_integer()}
| {:uploader, module()}
| {:filters, [module()]}
@type t :: %__MODULE__{
id: String.t(),
name: String.t(),
tempfile: String.t(),
content_type: String.t(),
path: String.t()
}
defstruct [:id, :name, :tempfile, :content_type, :path]
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
def store(upload, opts \\ []) do
opts = get_opts(opts)
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- MobilizonWeb.Upload.Filter.filter(opts.filters, upload),
{:ok, url_spec} <- MobilizonWeb.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
"type" => opts.activity_type,
"url" => [
%{
"type" => "Link",
"mediaType" => upload.content_type,
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
"name" => Map.get(opts, :description) || upload.name
}}
else
{:error, error} ->
Logger.error(
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
)
{:error, error}
end
end
def char_unescaped?(char) do
URI.char_unreserved?(char) or char == ?/
end
defp get_opts(opts) do
{size_limit, activity_type} =
case Keyword.get(opts, :type) do
:banner ->
{Mobilizon.CommonConfig.get!([:instance, :banner_upload_limit]), "Image"}
:avatar ->
{Mobilizon.CommonConfig.get!([:instance, :avatar_upload_limit]), "Image"}
_ ->
{Mobilizon.CommonConfig.get!([:instance, :upload_limit]), "Document"}
end
%{
activity_type: Keyword.get(opts, :activity_type, activity_type),
size_limit: Keyword.get(opts, :size_limit, size_limit),
uploader: Keyword.get(opts, :uploader, Mobilizon.CommonConfig.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Mobilizon.CommonConfig.get([__MODULE__, :filters])),
description: Keyword.get(opts, :description),
base_url:
Keyword.get(
opts,
:base_url,
Mobilizon.CommonConfig.get([__MODULE__, :base_url], MobilizonWeb.Endpoint.url())
)
}
end
defp prepare_upload(%Plug.Upload{} = file, opts) do
with :ok <- check_file_size(file.path, opts.size_limit),
{:ok, content_type, name} <- Mobilizon.MIME.file_mime_type(file.path, file.filename) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: name,
tempfile: file.path,
content_type: content_type
}}
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
:ok
else
false -> {:error, :file_too_large}
error -> error
end
end
defp check_file_size(_, _), do: :ok
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
URI.encode(path, &char_unescaped?/1) <>
if Mobilizon.CommonConfig.get([__MODULE__, :link_name], false) do
"?name=#{URI.encode(name, &char_unescaped?/1)}"
else
""
end
[base_url, "media", path]
|> Path.join()
end
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
end

View File

@ -0,0 +1,42 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/upload/filter.ex
defmodule MobilizonWeb.Upload.Filter do
@moduledoc """
Upload Filter behaviour
This behaviour allows to run filtering actions just before a file is uploaded. This allows to:
* morph in place the temporary file
* change any field of a `Mobilizon.Upload` struct
* cancel/stop the upload
"""
require Logger
@callback filter(MobilizonWeb.Upload.t()) ::
:ok | {:ok, MobilizonWeb.Upload.t()} | {:error, any()}
@spec filter([module()], MobilizonWeb.Upload.t()) ::
{:ok, MobilizonWeb.Upload.t()} | {:error, any()}
def filter([], upload) do
{:ok, upload}
end
def filter([filter | rest], upload) do
case filter.filter(upload) do
:ok ->
filter(rest, upload)
{:ok, upload} ->
filter(rest, upload)
error ->
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}")
error
end
end
end

View File

@ -0,0 +1,28 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/upload/filter/anonymize_filename.ex
defmodule MobilizonWeb.Upload.Filter.AnonymizeFilename do
@moduledoc """
Replaces the original filename with a pre-defined text or randomly generated string.
Should be used after `MobilizonWeb.Upload.Filter.Dedupe`.
"""
@behaviour MobilizonWeb.Upload.Filter
def filter(upload) do
extension = List.last(String.split(upload.name, "."))
name = Mobilizon.CommonConfig.get([__MODULE__, :text], random(extension))
{:ok, %MobilizonWeb.Upload{upload | name: name}}
end
defp random(extension) do
string =
10
|> :crypto.strong_rand_bytes()
|> Base.url_encode64(padding: false)
string <> "." <> extension
end
end

View File

@ -0,0 +1,19 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/upload/filter/dedupe.ex
defmodule MobilizonWeb.Upload.Filter.Dedupe do
@moduledoc """
Names the file after its hash to avoid dedupes
"""
@behaviour MobilizonWeb.Upload.Filter
alias MobilizonWeb.Upload
def filter(%Upload{name: name} = upload) do
extension = String.split(name, ".") |> List.last()
shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)
filename = shasum <> "." <> extension
{:ok, %Upload{upload | id: shasum, path: filename}}
end
end

View File

@ -0,0 +1,45 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/upload/filter/mogrify.ex
defmodule MobilizonWeb.Upload.Filter.Mogrify do
@moduledoc """
Handle mogrify transformations
"""
@behaviour MobilizonWeb.Upload.Filter
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
@type conversions :: conversion() | [conversion()]
def filter(%MobilizonWeb.Upload{tempfile: file, content_type: "image" <> _}) do
filters = Mobilizon.CommonConfig.get!([__MODULE__, :args])
file
|> Mogrify.open()
|> mogrify_filter(filters)
|> Mogrify.save(in_place: true)
:ok
end
def filter(_), do: :ok
defp mogrify_filter(mogrify, nil), do: mogrify
defp mogrify_filter(mogrify, [filter | rest]) do
mogrify
|> mogrify_filter(filter)
|> mogrify_filter(rest)
end
defp mogrify_filter(mogrify, []), do: mogrify
defp mogrify_filter(mogrify, {action, options}) do
Mogrify.custom(mogrify, action, options)
end
defp mogrify_filter(mogrify, action) when is_binary(action) do
Mogrify.custom(mogrify, action)
end
end

View File

@ -1,18 +0,0 @@
defmodule MobilizonWeb.UploadPlug do
@moduledoc """
Plug to intercept uploads
"""
use Plug.Builder
plug(Plug.Static,
at: "/",
from: {:mobilizon, "./uploads"}
)
# only: ~w(images robots.txt)
plug(:not_found)
def not_found(conn, _) do
send_resp(conn, 404, "not found")
end
end

View File

@ -1,53 +0,0 @@
defmodule MobilizonWeb.Uploaders.Avatar do
@moduledoc """
Handles avatar uploads
"""
use Arc.Definition
# Include ecto support (requires package arc_ecto installed):
# use Arc.Ecto.Definition
@versions [:original]
# To add a thumbnail version:
# @versions [:original, :thumb]
# Override the bucket on a per definition basis:
# def bucket do
# :custom_bucket_name
# end
# Whitelist file extensions:
# def validate({file, _}) do
# ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name))
# end
# Define a thumbnail transformation:
# def transform(:thumb, _) do
# {:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
# end
# Override the persisted filenames:
# def filename(version, _) do
# version
# end
# Override the storage directory:
# def storage_dir(version, {file, scope}) do
# "uploads/user/avatars/#{scope.id}"
# end
# Provide a default URL if there hasn't been a file uploaded
# def default_url(version, scope) do
# "/images/avatars/default_#{version}.png"
# end
# Specify custom headers for s3 objects
# Available options are [:cache_control, :content_disposition,
# :content_encoding, :content_length, :content_type,
# :expect, :expires, :storage_class, :website_redirect_location]
#
# def s3_object_headers(version, {file, scope}) do
# [content_type: MIME.from_path(file.file_name)]
# end
end

View File

@ -1,51 +0,0 @@
defmodule MobilizonWeb.Uploaders.Category do
@moduledoc """
Handles file uploads for categories
"""
use Arc.Definition
use Arc.Ecto.Definition
# To add a thumbnail version:
@versions [:original, :thumb]
@extension_whitelist ~w(.jpg .jpeg .gif .png)
# Override the bucket on a per definition basis:
# def bucket do
# :custom_bucket_name
# end
# Whitelist file extensions:
def validate({file, _}) do
file_extension = file.file_name |> Path.extname() |> String.downcase()
Enum.member?(@extension_whitelist, file_extension)
end
# Define a thumbnail transformation:
def transform(:thumb, _) do
{:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
end
# Override the persisted filenames:
def filename(version, {_file, %{title: title}}) do
"#{title}_#{version}"
end
# Override the storage directory:
def storage_dir(_, _) do
"uploads/event/"
end
# Provide a default URL if there hasn't been a file uploaded
# def default_url(version, scope) do
# "/images/avatars/default_#{version}.png"
# end
# Specify custom headers for s3 objects
# Available options are [:cache_control, :content_disposition,
# :content_encoding, :content_length, :content_type,
# :expect, :expires, :storage_class, :website_redirect_location]
#
# def s3_object_headers(version, {file, scope}) do
# [content_type: MIME.from_path(file.file_name)]
# end
end

View File

@ -0,0 +1,40 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/uploaders/local.ex
defmodule MobilizonWeb.Uploaders.Local do
@moduledoc """
Local uploader for files
"""
@behaviour MobilizonWeb.Uploaders.Uploader
def get_file(_) do
{:ok, {:static_dir, upload_path()}}
end
def put_file(upload) do
{local_path, file} =
case Enum.reverse(String.split(upload.path, "/", trim: true)) do
[file] ->
{upload_path(), file}
[file | folders] ->
path = Path.join([upload_path()] ++ Enum.reverse(folders))
File.mkdir_p!(path)
{path, file}
end
result_file = Path.join(local_path, file)
unless File.exists?(result_file) do
File.cp!(upload.tempfile, result_file)
end
:ok
end
def upload_path do
Mobilizon.CommonConfig.get!([__MODULE__, :uploads])
end
end

View File

@ -0,0 +1,73 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/uploaders/uploader.ex
defmodule MobilizonWeb.Uploaders.Uploader do
@moduledoc """
Defines the contract to put and get an uploaded file to any backend.
"""
@doc """
Instructs how to get the file from the backend.
Used by `MobilizonWeb.Plugs.UploadedMedia`.
"""
@type get_method :: {:static_dir, directory :: String.t()} | {:url, url :: String.t()}
@callback get_file(file :: String.t()) :: {:ok, get_method()}
@doc """
Put a file to the backend.
Returns:
* `:ok` which assumes `{:ok, upload.path}`
* `{:ok, spec}` where spec is:
* `{:file, filename :: String.t}` to handle reads with `get_file/1` (recommended)
This allows to correctly proxy or redirect requests to the backend, while allowing to migrate backends without breaking any URL.
* `{url, url :: String.t}` to bypass `get_file/2` and use the `url` directly in the activity.
* `{:error, String.t}` error information if the file failed to be saved to the backend.
* `:wait_callback` will wait for an http post request at `/api/pleroma/upload_callback/:upload_path` and call the uploader's `http_callback/3` method.
"""
@type file_spec :: {:file | :url, String.t()}
@callback put_file(MobilizonWeb.Upload.t()) ::
:ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback
@callback http_callback(Plug.Conn.t(), Map.t()) ::
{:ok, Plug.Conn.t()}
| {:ok, Plug.Conn.t(), file_spec()}
| {:error, Plug.Conn.t(), String.t()}
@optional_callbacks http_callback: 2
@spec put_file(module(), MobilizonWeb.Upload.t()) :: {:ok, file_spec()} | {:error, String.t()}
def put_file(uploader, upload) do
case uploader.put_file(upload) do
:ok -> {:ok, {:file, upload.path}}
:wait_callback -> handle_callback(uploader, upload)
{:ok, _} = ok -> ok
{:error, _} = error -> error
end
end
defp handle_callback(uploader, upload) do
:global.register_name({__MODULE__, upload.path}, self())
receive do
{__MODULE__, pid, conn, params} ->
case uploader.http_callback(conn, params) do
{:ok, conn, ok} ->
send(pid, {__MODULE__, conn})
{:ok, ok}
{:error, conn, error} ->
send(pid, {__MODULE__, conn})
{:error, error}
end
after
30_000 -> {:error, "Uploader callback timeout"}
end
end
end

View File

@ -8,15 +8,12 @@ defmodule MobilizonWeb.JsonLD.ObjectView do
def render("event.json", %{event: %Event{} = event}) do
# TODO: event.description is actually markdown!
json_ld = %{
"@context" => "https://schema.org",
"@type" => "Event",
"name" => event.title,
"description" => event.description,
"image" => [
event.thumbnail,
event.large_image
],
"performer" => %{
"@type" =>
if(event.organizer_actor.type == :Group, do: "PerformingGroup", else: "Person"),
@ -25,6 +22,15 @@ defmodule MobilizonWeb.JsonLD.ObjectView do
"location" => render_one(event.physical_address, ObjectView, "place.json", as: :address)
}
json_ld =
if event.picture do
Map.put(json_ld, "image", [
event.picture.file.url
])
else
json_ld
end
json_ld =
if event.begins_on,
do: Map.put(json_ld, "startDate", DateTime.to_iso8601(event.begins_on)),

View File

@ -44,7 +44,7 @@ defmodule MobilizonWeb.PageView do
end
def render("event.activity-json", %{conn: %{assigns: %{object: event}}}) do
event = Utils.make_event_data(event)
event = Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(event)
{:ok, html, []} = Earmark.as_html(event["summary"])
%{
@ -66,7 +66,7 @@ defmodule MobilizonWeb.PageView do
end
def render("comment.activity-json", %{conn: %{assigns: %{object: comment}}}) do
comment = Utils.make_comment_data(comment)
comment = Mobilizon.Service.ActivityPub.Converters.Comment.model_to_as(comment)
%{
"actor" => comment["actor"],

View File

@ -468,10 +468,24 @@ defmodule Mobilizon.Service.ActivityPub do
"""
@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_url: data["icon"]["url"],
banner_url: data["image"]["url"],
avatar: avatar,
banner: banner,
name: data["name"],
preferred_username: data["preferredUsername"],
summary: data["summary"],
@ -512,7 +526,7 @@ defmodule Mobilizon.Service.ActivityPub do
%Activity{
recipients: ["https://www.w3.org/ns/activitystreams#Public"],
actor: event.organizer_actor.url,
data: event |> make_event_data,
data: event |> Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(),
local: local
}
end
@ -523,7 +537,7 @@ defmodule Mobilizon.Service.ActivityPub do
%Activity{
recipients: ["https://www.w3.org/ns/activitystreams#Public"],
actor: comment.actor.url,
data: comment |> make_comment_data,
data: comment |> Mobilizon.Service.ActivityPub.Converters.Comment.model_to_as(),
local: local
}
end

View File

@ -0,0 +1,9 @@
defmodule Mobilizon.Service.ActivityPub.Converter do
@moduledoc """
Converter behaviour
This module allows to convert from ActivityStream format to our own internal one, and back
"""
@callback as_to_model_data(map()) :: map()
@callback model_to_as(struct()) :: map()
end

View File

@ -0,0 +1,47 @@
defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
@moduledoc """
Actor converter
This module allows to convert events from ActivityStream format to our own internal one, and back
"""
alias Mobilizon.Actors.Actor, as: ActorModel
alias Mobilizon.Service.ActivityPub.Converter
@behaviour Converter
@doc """
Converts an AP object data to our internal data structure
"""
@impl Converter
@spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do
%{
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferred_username"],
"summary" => object["summary"],
"url" => object["url"],
"name" => object["name"]
}
end
@doc """
Convert an actor struct to an ActivityStream representation
"""
@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,
"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
}
end
end

View File

@ -0,0 +1,96 @@
defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
@moduledoc """
Comment converter
This module allows to convert events from ActivityStream format to our own internal one, and back
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment, as: CommentModel
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
require Logger
@behaviour Converter
@doc """
Converts an AP object data to our internal data structure
"""
@impl Converter
@spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do
{:ok, %Actor{id: actor_id}} = Actors.get_or_fetch_by_url(object["actor"])
Logger.debug("Inserting full comment")
Logger.debug(inspect(object))
data = %{
"text" => object["content"],
"url" => object["id"],
"actor_id" => actor_id,
"in_reply_to_comment_id" => nil,
"event_id" => nil,
"uuid" => object["uuid"]
}
# We fetch the parent object
Logger.debug("We're fetching the parent object")
data =
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
object["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
# Reply to an event (Event)
{:ok, %Event{id: id}} ->
Logger.debug("Parent object is an event")
data |> Map.put("event_id", id)
# Reply to a comment (Comment)
{:ok, %CommentModel{id: id} = comment} ->
Logger.debug("Parent object is another comment")
data
|> Map.put("in_reply_to_comment_id", id)
|> Map.put("origin_comment_id", comment |> CommentModel.get_thread_id())
# Anything else is kind of a MP
{:error, parent} ->
Logger.debug("Parent object is something we don't handle")
Logger.debug(inspect(parent))
data
end
else
Logger.debug("No parent object for this comment")
data
end
data
end
@doc """
Make an AS comment object from an existing `Comment` structure.
"""
@impl Converter
@spec model_to_as(CommentModel.t()) :: map()
def model_to_as(%CommentModel{} = comment) do
object = %{
"type" => "Note",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"content" => comment.text,
"actor" => comment.actor.url,
"attributedTo" => comment.actor.url,
"uuid" => comment.uuid,
"id" => Routes.page_url(Endpoint, :comment, comment.uuid)
}
if comment.in_reply_to_comment do
object |> Map.put("inReplyTo", comment.in_reply_to_comment.url || comment.event.url)
else
object
end
end
end

View File

@ -0,0 +1,70 @@
defmodule Mobilizon.Service.ActivityPub.Converters.Event do
@moduledoc """
Event converter
This module allows to convert events from ActivityStream format to our own internal one, and back
"""
alias Mobilizon.Actors
alias Mobilizon.Media
alias Mobilizon.Media.Picture
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Service.ActivityPub.Converter
@behaviour Converter
@doc """
Converts an AP object data to our internal data structure
"""
@impl Converter
@spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do
with {:ok, %Actor{id: actor_id}} <- Actors.get_actor_by_url(object["actor"]) do
picture_id =
with true <- Map.has_key?(object, "attachment"),
%Picture{id: picture_id} <-
Media.get_picture_by_url(
object["attachment"]
|> hd
|> Map.get("url")
|> hd
|> Map.get("href")
) do
picture_id
else
_ -> nil
end
%{
"title" => object["name"],
"description" => object["content"],
"organizer_actor_id" => actor_id,
"picture_id" => picture_id,
"begins_on" => object["begins_on"],
"category" => object["category"],
"url" => object["id"],
"uuid" => object["uuid"]
}
end
end
@doc """
Convert an event struct to an ActivityStream representation
"""
@impl Converter
@spec model_to_as(EventModel.t()) :: map()
def model_to_as(%EventModel{} = event) do
%{
"type" => "Event",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"title" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
"category" => event.category,
"summary" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> DateTime.to_iso8601(),
"updated_at" => event.updated_at |> DateTime.to_iso8601(),
"id" => event.url
}
end
end

View File

@ -15,9 +15,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Events.Comment
alias Mobilizon.Media.Picture
alias Mobilizon.Events
alias Mobilizon.Activity
alias Mobilizon.Service.ActivityPub
alias Ecto.Changeset
require Logger
alias MobilizonWeb.Router.Helpers, as: Routes
@ -108,23 +108,6 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Map.put_new_lazy(map, "published", &make_date/0)
end
@doc """
Converts an AP object data to our internal data structure
"""
def object_to_event_data(object) do
{:ok, %Actor{id: actor_id}} = Actors.get_actor_by_url(object["actor"])
%{
"title" => object["name"],
"description" => object["content"],
"organizer_actor_id" => actor_id,
"begins_on" => object["begins_on"],
"category" => object["category"],
"url" => object["id"],
"uuid" => object["uuid"]
}
end
@doc """
Inserts a full object if it is contained in an activity.
"""
@ -135,7 +118,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"""
def insert_full_object(%{"object" => %{"type" => "Event"} = object_data})
when is_map(object_data) do
with object_data <- object_to_event_data(object_data),
with object_data <-
Mobilizon.Service.ActivityPub.Converters.Event.as_to_model_data(object_data),
{:ok, _} <- Events.create_event(object_data) do
:ok
end
@ -155,60 +139,14 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"""
def insert_full_object(%{"object" => %{"type" => "Note"} = object_data})
when is_map(object_data) do
Logger.debug("Inserting full comment")
Logger.debug(inspect(object_data))
with {:ok, %Actor{id: actor_id}} <- Actors.get_or_fetch_by_url(object_data["actor"]) do
data = %{
"text" => object_data["content"],
"url" => object_data["id"],
"actor_id" => actor_id,
"in_reply_to_comment_id" => nil,
"event_id" => nil,
"uuid" => object_data["uuid"]
}
# We fetch the parent object
Logger.debug("We're fetching the parent object")
data =
if Map.has_key?(object_data, "inReplyTo") && object_data["inReplyTo"] != nil &&
object_data["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object_data["inReplyTo"]}" end)
case ActivityPub.fetch_object_from_url(object_data["inReplyTo"]) do
# Reply to an event (Comment)
{:ok, %Event{id: id}} ->
Logger.debug("Parent object is an event")
data |> Map.put("event_id", id)
# Reply to a comment (Comment)
{:ok, %Comment{id: id} = comment} ->
Logger.debug("Parent object is another comment")
data
|> Map.put("in_reply_to_comment_id", id)
|> Map.put("origin_comment_id", comment |> Comment.get_thread_id())
# Anything else is kind of a MP
{:error, object} ->
Logger.debug("Parent object is something we don't handle")
Logger.debug(inspect(object))
data
end
else
Logger.debug("No parent object for this comment")
data
end
with {:ok, _comment} <- Events.create_comment(data) do
:ok
else
err ->
Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err))
{:error, err}
end
with data <- Mobilizon.Service.ActivityPub.Converters.Comment.as_to_model_data(object_data),
{:ok, _comment} <- Events.create_comment(data) do
:ok
else
err ->
Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err))
{:error, err}
end
end
@ -238,6 +176,43 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
# Repo.one(query)
# end
def make_picture_data(%Plug.Upload{} = picture) do
with {:ok, picture} <- MobilizonWeb.Upload.store(picture) do
picture
else
_ -> nil
end
end
def make_picture_data(%Picture{file: file} = _picture) do
%{
"type" => "Document",
"url" => [
%{
"type" => "Link",
"mediaType" => file.content_type,
"href" => file.url
}
],
"name" => file.name
}
end
def make_picture_data(%{picture: picture}) do
with {:ok, %{"url" => [%{"href" => url}]}} <- MobilizonWeb.Upload.store(picture.file),
{:ok, %Picture{file: _file} = pic} <-
Mobilizon.Media.create_picture(%{
"file" => %{
"url" => url,
"name" => picture.name
}
}) do
make_picture_data(pic)
end
end
def make_picture_data(nil), do: nil
@doc """
Make an AP event object from an set of values
"""
@ -246,6 +221,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
String.t(),
String.t(),
String.t(),
map(),
list(),
list(),
map(),
@ -256,7 +232,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
to,
title,
content_html,
# attachments,
picture \\ nil,
tags \\ [],
# _cw \\ nil,
cc \\ [],
@ -266,14 +242,13 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Logger.debug("Making event data")
uuid = Ecto.UUID.generate()
%{
res = %{
"type" => "Event",
"to" => to,
"cc" => cc,
"content" => content_html,
"name" => title,
# "summary" => cw,
# "attachment" => attachments,
"begins_on" => metadata.begins_on,
"category" => category,
"actor" => actor,
@ -281,55 +256,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"uuid" => uuid,
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
end
@spec make_event_data(Event.t(), list(String.t())) :: map()
def make_event_data(
%Event{} = event,
to \\ ["https://www.w3.org/ns/activitystreams#Public"]
) do
%{
"type" => "Event",
"to" => to,
"title" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
"category" => event.category,
"summary" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> DateTime.to_iso8601(),
"updated_at" => event.updated_at |> DateTime.to_iso8601(),
"id" => Routes.page_url(Endpoint, :event, event.uuid)
}
end
@doc """
Make an AP comment object from an existing `Comment` structure.
"""
def make_comment_data(
%Comment{
text: text,
actor: actor,
uuid: uuid,
in_reply_to_comment: reply_to,
event: event
},
to \\ ["https://www.w3.org/ns/activitystreams#Public"]
) do
object = %{
"type" => "Note",
"to" => to,
"content" => text,
"actor" => actor.url,
"attributedTo" => actor.url,
"uuid" => uuid,
"id" => Routes.page_url(Endpoint, :comment, uuid)
}
if reply_to do
object |> Map.put("inReplyTo", reply_to.url || event.url)
else
object
end
if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
end
@doc """

View File

@ -74,12 +74,19 @@ defmodule Mobilizon.Service.Export.Feed do
|> Feed.generator("Mobilizon", uri: "https://joinmobilizon.org", version: version())
|> Feed.entries(Enum.map(events, &get_entry/1))
feed = if actor.avatar_url, do: Feed.icon(feed, actor.avatar_url), else: feed
feed =
if actor.avatar do
Feed.icon(feed, actor.avatar.url)
else
feed
end
feed =
if actor.banner_url,
do: Feed.logo(feed, actor.banner_url),
else: feed
if actor.banner do
Feed.logo(feed, actor.banner.url)
else
feed
end
feed
|> Feed.build()
@ -113,7 +120,8 @@ defmodule Mobilizon.Service.Export.Feed do
@spec fetch_events_from_token(String.t()) :: String.t()
defp fetch_events_from_token(token) do
with %FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(token) do
with {:ok, _uuid} <- Ecto.UUID.cast(token),
%FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(token) do
case actor do
%Actor{} = actor ->
events = fetch_identity_going_to_events(actor)

View File

@ -65,7 +65,7 @@ defmodule Mobilizon.Service.Formatter do
@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@uri_schemes Application.get_env(:mobilizon, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
# # TODO: make it use something other than @link_regex

View File

@ -3,14 +3,19 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Actors.Actor do
alias Mobilizon.Actors.Actor
def build_tags(%Actor{} = actor) do
[
tags = [
Tag.tag(:meta, property: "og:title", content: Actor.display_name_and_username(actor)),
Tag.tag(:meta, property: "og:url", content: actor.url),
Tag.tag(:meta, property: "og:description", content: actor.summary),
Tag.tag(:meta, property: "og:type", content: "profile"),
Tag.tag(:meta, property: "profile:username", content: actor.preferred_username),
Tag.tag(:meta, property: "og:image", content: actor.avatar_url),
Tag.tag(:meta, property: "twitter:card", content: "summary")
]
if is_nil(actor.avatar) do
tags
else
tags ++ [Tag.tag(:meta, property: "og:image", content: actor.avatar.url)]
end
end
end

View File

@ -5,16 +5,28 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
alias MobilizonWeb.JsonLD.ObjectView
def build_tags(%Event{} = event) do
[
tags = [
Tag.tag(:meta, property: "og:title", content: event.title),
Tag.tag(:meta, property: "og:url", content: event.url),
Tag.tag(:meta, property: "og:description", content: event.description),
Tag.tag(:meta, property: "og:type", content: "website"),
Tag.tag(:meta, property: "og:image", content: event.thumbnail),
Tag.tag(:meta, property: "og:image", content: event.large_image),
Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
~s{<script type="application/ld+json">#{json(event)}</script>} |> HTML.raw()
Tag.tag(:meta, property: "og:type", content: "website")
]
tags =
if is_nil(event.picture) do
tags
else
tags ++
[
Tag.tag(:meta, property: "og:image", content: event.picture.file.url)
]
end
tags ++
[
Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
~s{<script type="application/ld+json">#{json(event)}</script>} |> HTML.raw()
]
end
# Insert JSON-LD schema by hand because Tag.content_tag wants to escape it

View File

@ -84,13 +84,12 @@ defmodule Mobilizon.Mixfile do
{:absinthe_plug, "~> 1.4.6"},
{:absinthe_ecto, "~> 0.1.3"},
{:dataloader, "~> 1.0.6"},
{:arc, "~> 0.11.0"},
{:arc_ecto, "~> 0.11.0"},
{:plug_cowboy, "~> 2.0"},
{:atomex, "0.3.0"},
{:cachex, "~> 3.1"},
{:earmark, "~> 1.3.1"},
{:geohax, "~> 0.3.0"},
{:mogrify, "~> 0.7.2"},
# Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:ex_machina, "~> 2.3", only: [:dev, :test]},
@ -226,7 +225,6 @@ defmodule Mobilizon.Mixfile do
MobilizonWeb.Guardian.Plug,
MobilizonWeb.JsonLD.ObjectView,
MobilizonWeb.PageController,
MobilizonWeb.UploadPlug,
MobilizonWeb.Uploaders.Avatar,
MobilizonWeb.Uploaders.Category,
MobilizonWeb.Uploaders.Category.Type

View File

@ -1,11 +1,11 @@
%{
"absinthe": {:hex, :absinthe, "1.4.16", "0933e4d9f12652b12115d5709c0293a1bf78a22578032e9ad0dad4efee6b9eb1", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"absinthe_ecto": {:hex, :absinthe_ecto, "0.1.3", "420b68129e79fe4571a4838904ba03e282330d335da47729ad52ffd7b8c5fcb1", [:mix], [{:absinthe, "~> 1.3.0 or ~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_phoenix": {:hex, :absinthe_phoenix, "1.4.3", "cea34e7ebbc9a252038c1f1164878ee86bcb108905fe462be77efacda15c1e70", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4.0", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.10.5 or ~> 2.11", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_plug": {:hex, :absinthe_plug, "1.4.6", "ac5d2d3d02acf52fda0f151b294017ab06e2ed1c6c15334e06aac82c94e36e08", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"arc": {:hex, :arc, "0.11.0", "ac7a0cc03035317b6fef9fe94c97d7d9bd183a3e7ce1606aa0c175cfa8d1ba6d", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.0", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"},
"arc_ecto": {:hex, :arc_ecto, "0.11.1", "27aedf8c236b2097eed09d96f4ae73b43eb4c042a0e2ae42d44bf644cf16115c", [:mix], [{:arc, "~> 0.11.0", [hex: :arc, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm"},
"argon2_elixir": {:hex, :argon2_elixir, "2.0.3", "f2272c89d6a84f85c22b9b83912fd60740bf6052bf0078e621a6e9d1127e25c8", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_phoenix": {:hex, :absinthe_phoenix, "1.4.4", "af3b7b44483121f756ea0ec75a536b74f67cdd62ec6a34b9e58df1fb4662389e", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4.0", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_plug": {:hex, :absinthe_plug, "1.4.7", "939b6b9e1c7abc6b399a5b49faa690a1fbb55b195c670aa35783b14b08ccec7a", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"arc": {:git, "https://github.com/tcitworld/arc.git", "f5788de02935bcbd38941d964b9bdd8833fe263d", []},
"arc_ecto": {:git, "https://github.com/tcitworld/arc_ecto.git", "e0d8db119c564744404cff68157417e2a83941af", []},
"argon2_elixir": {:hex, :argon2_elixir, "2.0.5", "0073a87d755c7e63fc4f9d08b1d1646585b93f144cecde126e15061b24240b20", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"atomex": {:hex, :atomex, "0.3.0", "19b5d1a2aef8706dbd307385f7d5d9f6f273869226d317492c396c7bacf26402", [:mix], [{:xml_builder, "~> 2.0.0", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm"},
"bamboo": {:hex, :bamboo, "1.2.0", "8aebd24f7c606c32d0163c398004a11608ca1028182a169b2e527793bfab7561", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"bamboo_smtp": {:hex, :bamboo_smtp, "1.6.0", "0a3607b77f22554af58c547350c1c73ebba6f4fb2c4bd0b11713ab5b4081588f", [:mix], [{:bamboo, "~> 1.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.12.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"},
@ -25,7 +25,7 @@
"decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "3.0.8", "9eb6a1fcfc593e6619d45ef51afe607f1554c21ca188a1cd48eecc27223567f1", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"ecto": {:hex, :ecto, "3.0.9", "f01922a0b91a41d764d4e3a914d7f058d99a03460d3082c61dd2dcadd724c934", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_autoslug_field": {:hex, :ecto_autoslug_field, "1.0.0", "577eed25e6d045b8d783f82c9872f97c3a84017a4feae50eaf3cf4e1334a19e2", [:mix], [{:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:slugger, ">= 0.2.0", [hex: :slugger, repo: "hexpm", optional: false]}], "hexpm"},
"ecto_enum": {:hex, :ecto_enum, "1.2.0", "9ead3ee04efc4cb68a50560a9d9ebb665dd697f957f1c3df8e81bf863cf7a4e9", [:mix], [{:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
@ -36,15 +36,16 @@
"ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm"},
"ex_image_info": {:hex, :ex_image_info, "0.2.4", "610002acba43520a9b1cf1421d55812bde5b8a8aeaf1fe7b1f8823e84e762adb", [:mix], [], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
"ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm"},
"exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.10.6", "e2b9718c9d8e3ef90bc22278c3f76c850a9f9116faf4ebe9678063310742edc2", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"exgravatar": {:hex, :exgravatar, "2.0.1", "66d595c7d63dd6bbac442c5542a724375ae29144059c6fe093e61553850aace4", [:mix], [], "hexpm"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"exvcr": {:hex, :exvcr, "0.10.3", "1ae3b97560430acfa88ebc737c85b2b7a9dbacd8a2b26789a19718b51ae3522c", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"feeder": {:hex, :feeder, "2.2.4", "56ec535cf2f79719bc53b5c2abe5f6cf481fc01e5ae6229ab7cc829644f039ec", [:make], [], "hexpm"},
"file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"},
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
"gen_smtp": {:hex, :gen_smtp, "0.12.0", "97d44903f5ca18ca85cb39aee7d9c77e98d79804bbdef56078adcf905cb2ef00", [:rebar3], [], "hexpm"},
"geo": {:hex, :geo, "3.1.0", "727e005262430d037e870ff364e65d80ca5ca21d5ac8eddd57a1ada72c3f83b0", [:mix], [], "hexpm"},
"geo_postgis": {:hex, :geo_postgis, "3.1.0", "d06c8fa5fd140a52a5c9dab4ad6623a696dd7d99dd791bb361d3f94942442ff9", [:mix], [{:geo, "~> 3.1", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"},
@ -72,19 +73,20 @@
"mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
"mmdb2_decoder": {:hex, :mmdb2_decoder, "1.0.0", "48929cdadae9dd5a9705133dff024763774a2615f35354912832b98e72261110", [:mix], [], "hexpm"},
"mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mogrify": {:hex, :mogrify, "0.7.2", "4d00b60288e338028e2af4cccff9b0da365d83b7e5da52e58fb2de513ef5fedd", [:mix], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.4.3", "8eed4a64ff1e12372cd634724bddd69185938f52c18e1396ebac76375d85677d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
"phoenix": {:hex, :phoenix, "1.4.6", "8535f4a01291f0fbc2c30c78c4ca6a2eacc148db5178ad76e8b2fc976c590115", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.13.2", "f5d27c9b10ce881a60177d2b5227314fc60881e6b66b41dfe3349db6ed06cf57", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.0", "3bb31a9fbd40ffe8652e60c8660dffd72dd231efcdf49b744fb75b9ef7db5dd2", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
"plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.14.2", "6680591bbce28d92f043249205e8b01b36cab9ef2a7911abc43649242e1a3b78", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"rdf": {:hex, :rdf, "0.6.0", "e0d9098c157b91b9e7318a2fca18cc2e2b178e1290c5cfbb014cf2077c4aa778", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
"rsa_ex": {:hex, :rsa_ex, "0.4.0", "e28dd7dc5236e156df434af0e4aa822384c8866c928e17b785d4edb7c253b558", [:mix], [], "hexpm"},

View File

@ -0,0 +1,41 @@
defmodule Mobilizon.Repo.Migrations.CreatePictures do
use Ecto.Migration
def up do
create table(:pictures) do
add(:file, :map)
timestamps()
end
alter table(:actors) do
remove(:avatar_url)
remove(:banner_url)
add(:avatar, :map)
add(:banner, :map)
end
alter table(:events) do
remove(:thumbnail)
remove(:large_image)
add(:picture_id, references(:pictures, on_delete: :delete_all))
end
end
def down do
alter table(:actors) do
add(:avatar_url, :string)
add(:banner_url, :string)
remove(:avatar)
remove(:banner)
end
alter table(:events) do
add(:large_image, :string)
add(:thumbnail, :string)
remove(:picture_id)
end
drop(table(:pictures))
end
end

View File

@ -1,5 +1,5 @@
# source: http://localhost:4001/api
# timestamp: Fri Apr 26 2019 14:47:01 GMT+0200 (heure dété dEurope centrale)
# source: http://localhost:4000/api
# timestamp: Thu May 02 2019 16:24:47 GMT+0200 (GMT+02:00)
schema {
query: RootQueryType
@ -8,11 +8,11 @@ schema {
"""An ActivityPub actor"""
interface Actor {
"""The actor's avatar url"""
avatarUrl: String
"""The actor's avatar picture"""
avatar: Picture
"""The actor's banner url"""
bannerUrl: String
"""The actor's banner picture"""
banner: Picture
"""The actor's domain if (null if it's this instance)"""
domain: String
@ -193,9 +193,6 @@ type Event {
"""Internal ID for this event"""
id: Int
"""A large picture for the event"""
largeImage: String
"""Whether the event is local or not"""
local: Boolean
@ -214,6 +211,9 @@ type Event {
"""The type of the event's address"""
physicalAddress: Address
"""The event's picture"""
picture: Picture
"""When the event was published"""
publishAt: DateTime
@ -229,9 +229,6 @@ type Event {
"""The event's tags"""
tags: [Tag]
"""A thumbnail picture for the event"""
thumbnail: String
"""The event's title"""
title: String
@ -319,11 +316,11 @@ Represents a group of actors
"""
type Group implements Actor {
"""The actor's avatar url"""
avatarUrl: String
"""The actor's avatar picture"""
avatar: Picture
"""The actor's banner url"""
bannerUrl: String
"""The actor's banner picture"""
banner: Picture
"""The actor's domain if (null if it's this instance)"""
domain: String
@ -465,11 +462,11 @@ Represents a person identity
"""
type Person implements Actor {
"""The actor's avatar url"""
avatarUrl: String
"""The actor's avatar picture"""
avatar: Picture
"""The actor's banner url"""
bannerUrl: String
"""The actor's banner picture"""
banner: Picture
"""The actor's domain if (null if it's this instance)"""
domain: String
@ -546,6 +543,37 @@ type PhoneAddress {
phone: String
}
"""A picture"""
type Picture {
"""The picture's alternative text"""
alt: String
"""The picture's UUID"""
id: UUID
"""The picture's name"""
name: String
"""The picture's full thumbnail URL"""
thumbnailUrl: String
"""The picture's full URL"""
url: String
}
"""An attached picture or a link to a picture"""
input PictureInput {
picture: PictureInputObject
pictureId: String
}
"""An attached picture"""
input PictureInputObject {
alt: String
file: Upload!
name: String!
}
"""
The `Point` scalar type represents Point geographic information compliant string data,
represented as floats separated by a semi-colon. The geodetic system is WGS 84
@ -560,7 +588,25 @@ type RootMutationType {
createComment(actorUsername: String!, text: String!): Comment
"""Create an event"""
createEvent(beginsOn: DateTime!, category: String!, description: String!, endsOn: DateTime, largeImage: String, onlineAddress: String, organizerActorId: ID!, phoneAddress: String, public: Boolean, publishAt: DateTime, state: Int, status: Int, thumbnail: String, title: String!): Event
createEvent(
beginsOn: DateTime!
category: String!
description: String!
endsOn: DateTime
onlineAddress: String
organizerActorId: ID!
phoneAddress: String
"""
The picture for the event, either as an object or directly the ID of an existing Picture
"""
picture: PictureInput
public: Boolean
publishAt: DateTime
state: Int
status: Int
title: String!
): Event
"""Create a Feed Token"""
createFeedToken(actorId: Int): FeedToken
@ -572,6 +618,16 @@ type RootMutationType {
"""
adminActorUsername: String
"""
The avatar for the group, either as an object or directly the ID of an existing Picture
"""
avatar: PictureInput
"""
The banner for the group, either as an object or directly the ID of an existing Picture
"""
banner: PictureInput
"""The summary for the group"""
description: String = ""
@ -584,6 +640,16 @@ type RootMutationType {
"""Create a new person for user"""
createPerson(
"""
The avatar for the profile, either as an object or directly the ID of an existing Picture
"""
avatar: PictureInput
"""
The banner for the profile, either as an object or directly the ID of an existing Picture
"""
banner: PictureInput
"""The displayed name for the new profile"""
name: String = ""
preferredUsername: String!
@ -621,6 +687,16 @@ type RootMutationType {
"""Register a first profile on registration"""
registerPerson(
"""
The avatar for the profile, either as an object or directly the ID of an existing Picture
"""
avatar: PictureInput
"""
The banner for the profile, either as an object or directly the ID of an existing Picture
"""
banner: PictureInput
"""The email from the user previously created"""
email: String!
@ -641,6 +717,9 @@ type RootMutationType {
"""Send a link through email to reset user password"""
sendResetPassword(email: String!, locale: String = "en"): String
"""Upload a picture"""
uploadPicture(alt: String, file: Upload!, name: String!): Picture
"""Validate an user after registration"""
validateUser(token: String!): Login
}
@ -680,6 +759,9 @@ type RootQueryType {
"""Get a person by it's preferred username"""
person(preferredUsername: String!): Person
"""Get a picture"""
picture(id: String!): Picture
"""Reverse geocode coordinates"""
reverseGeocode(latitude: Float!, longitude: Float!): [Address]
@ -731,6 +813,12 @@ type Tag {
title: String
}
"""
Represents an uploaded file.
"""
scalar Upload
"""A local user of Mobilizon"""
type User {
"""The datetime the last activation/confirmation token was sent"""

BIN
test/fixtures/image.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,10 +1,10 @@
{
"type": "Update",
"object": {
"url": "http://mastodon.example.org/@gargron",
"type": "Person",
"summary": "<p>Some bio</p>",
"publicKey": {
{
"type": "Update",
"object": {
"url": "http://mastodon.example.org/@gargron",
"type": "Person",
"summary": "<p>Some bio</p>",
"publicKey": {
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n",
"owner": "http://mastodon.example.org/users/gargron",
"id": "http://mastodon.example.org/users/gargron#main-key"
@ -20,7 +20,16 @@
"endpoints": {
"sharedInbox": "http://mastodon.example.org/inbox"
},
"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}
"icon":{
"type":"Image",
"mediaType":"image/png",
"url":"https://files.mastodon.social/accounts/avatars/000/000/001/original/a285c086605e4182.png"
},
"image":{
"type":"Image",
"mediaType":"image/png",
"url":"https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png"
}
},
"id": "http://mastodon.example.org/users/gargron#updates/1519563538",
"actor": "http://mastodon.example.org/users/gargron",

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

1
test/fixtures/test.txt vendored Normal file
View File

@ -0,0 +1 @@
this is a text file

1
test/fixtures/test_tmp.txt vendored Normal file
View File

@ -0,0 +1 @@
this is a text file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,156 +0,0 @@
[
{
"request": {
"body": "",
"headers": {
"Accept": "application/activity+json"
},
"method": "get",
"options": {
"follow_redirect": "true",
"recv_timeout": 20000,
"connect_timeout": 10000
},
"request_body": "",
"url": "https://social.tcit.fr/users/tcit/statuses/101160654038714030"
},
"response": {
"binary": false,
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"sensitive\":\"as:sensitive\",\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"Hashtag\":\"as:Hashtag\",\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"toot\":\"http://joinmastodon.org/ns#\",\"Emoji\":\"toot:Emoji\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"},\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\"}],\"id\":\"https://social.tcit.fr/users/tcit/statuses/101160654038714030\",\"type\":\"Note\",\"summary\":null,\"inReplyTo\":\"https://social.tcit.fr/users/tcit/statuses/101160195754333819\",\"published\":\"2018-11-30T14:44:41Z\",\"url\":\"https://social.tcit.fr/@tcit/101160654038714030\",\"attributedTo\":\"https://social.tcit.fr/users/tcit\",\"to\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"cc\":[\"https://social.tcit.fr/users/tcit/followers\"],\"sensitive\":false,\"atomUri\":\"https://social.tcit.fr/users/tcit/statuses/101160654038714030\",\"inReplyToAtomUri\":\"https://social.tcit.fr/users/tcit/statuses/101160195754333819\",\"conversation\":\"tag:social.tcit.fr,2018-11-30:objectId=3642669:objectType=Conversation\",\"content\":\"\\u003cp\\u003eOkay so that\\u0026apos;s it.\\u003cbr /\\u003e\\u003ca href=\\\"https://tcit.frama.io/group-uri-scheme/draft-tcit-group-uri-01.txt\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"ellipsis\\\"\\u003etcit.frama.io/group-uri-scheme\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e/draft-tcit-group-uri-01.txt\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\",\"contentMap\":{\"fr\":\"\\u003cp\\u003eOkay so that\\u0026apos;s it.\\u003cbr /\\u003e\\u003ca href=\\\"https://tcit.frama.io/group-uri-scheme/draft-tcit-group-uri-01.txt\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"ellipsis\\\"\\u003etcit.frama.io/group-uri-scheme\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e/draft-tcit-group-uri-01.txt\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\"},\"attachment\":[],\"tag\":[]}",
"headers": {
"Date": "Tue, 04 Dec 2018 13:59:58 GMT",
"Content-Type": "application/activity+json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Link": "<https://social.tcit.fr/users/tcit/updates/15979.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://social.tcit.fr/users/tcit/statuses/101160654038714030>; rel=\"alternate\"; type=\"application/activity+json\"",
"Vary": "Accept,Accept-Encoding",
"Cache-Control": "max-age=180, public",
"ETag": "W/\"619af54f65bbb41538e430b8247c36d7\"",
"X-Request-Id": "84a750de-2dfa-4a36-976e-bae0b0ac4821",
"X-Runtime": "0.056423",
"X-Cached": "MISS"
},
"status_code": 200,
"type": "ok"
}
},
{
"request": {
"body": "",
"headers": {
"Accept": "application/activity+json"
},
"method": "get",
"options": {
"follow_redirect": "true"
},
"request_body": "",
"url": "https://social.tcit.fr/users/tcit"
},
"response": {
"binary": false,
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"sensitive\":\"as:sensitive\",\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"Hashtag\":\"as:Hashtag\",\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"toot\":\"http://joinmastodon.org/ns#\",\"Emoji\":\"toot:Emoji\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"},\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\"}],\"id\":\"https://social.tcit.fr/users/tcit\",\"type\":\"Person\",\"following\":\"https://social.tcit.fr/users/tcit/following\",\"followers\":\"https://social.tcit.fr/users/tcit/followers\",\"inbox\":\"https://social.tcit.fr/users/tcit/inbox\",\"outbox\":\"https://social.tcit.fr/users/tcit/outbox\",\"featured\":\"https://social.tcit.fr/users/tcit/collections/featured\",\"preferredUsername\":\"tcit\",\"name\":\"🦄 Thomas Citharel\",\"summary\":\"\\u003cp\\u003eHoping to make people\\u0026apos;s life better with free software at \\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e.\\u003c/p\\u003e\",\"url\":\"https://social.tcit.fr/@tcit\",\"manuallyApprovesFollowers\":false,\"publicKey\":{\"id\":\"https://social.tcit.fr/users/tcit#main-key\",\"owner\":\"https://social.tcit.fr/users/tcit\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApXwYMUdFg3XUd+bGsh8C\\nyiMRGpRGAWuCdM5pDWx5uM4pW2pM3xbHbcI21j9h8BmlAiPg6hbZD73KGly2N8Rt\\n5iIS0I+l6i8kA1JCCdlAaDTRd41RKMggZDoQvjVZQtsyE1VzMeU2kbqqTFN6ew7H\\nvbd6O0NhixoKoZ5f3jwuBDZoT0p1TAcaMdmG8oqHD97isizkDnRn8cOBA6wtI+xb\\n5xP2zxZMsLpTDZLiKU8XcPKZCw4OfQfmDmKkHtrFb77jCAQj/s/FxjVnvxRwmfhN\\nnWy0D+LUV/g63nHh/b5zXIeV92QZLvDYbgbezmzUzv9UeA1s70GGbaDqCIy85gw9\\n+wIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[],\"attachment\":[{\"type\":\"PropertyValue\",\"name\":\"Works at\",\"value\":\"\\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Pronouns\",\"value\":\"He/Him\"},{\"type\":\"PropertyValue\",\"name\":\"Work Account\",\"value\":\"\\u003ca href=\\\"https://framapiaf.org/@tcit\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003eframapiaf.org/@tcit\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Site\",\"value\":\"\\u003ca href=\\\"https://tcit.fr\\\" rel=\\\"me nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"\\\"\\u003etcit.fr\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e\"}],\"endpoints\":{\"sharedInbox\":\"https://social.tcit.fr/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://media.social.tcit.fr/mastodontcit/accounts/avatars/000/000/001/original/a28c50ce5f2b13fd.jpg\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://media.social.tcit.fr/mastodontcit/accounts/headers/000/000/001/original/4d1ab77c20265ee9.jpg\"}}",
"headers": {
"Date": "Tue, 04 Dec 2018 13:59:58 GMT",
"Content-Type": "application/activity+json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Link": "<https://social.tcit.fr/.well-known/webfinger?resource=acct%3Atcit%40social.tcit.fr>; rel=\"lrdd\"; type=\"application/xrd+xml\", <https://social.tcit.fr/users/tcit.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://social.tcit.fr/users/tcit>; rel=\"alternate\"; type=\"application/activity+json\"",
"Vary": "Accept,Accept-Encoding",
"Cache-Control": "max-age=180, public",
"ETag": "W/\"039b9e136f81a55656fb1f38a23640d2\"",
"X-Request-Id": "91a50164-aa87-45c9-8100-786b9c74fbe0",
"X-Runtime": "0.039489",
"X-Cached": "MISS"
},
"status_code": 200,
"type": "ok"
}
},
{
"request": {
"body": "",
"headers": {
"Accept": "application/activity+json"
},
"method": "get",
"options": {
"follow_redirect": "true",
"recv_timeout": 20000,
"connect_timeout": 10000
},
"request_body": "",
"url": "https://social.tcit.fr/users/tcit/statuses/101160195754333819"
},
"response": {
"binary": false,
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"sensitive\":\"as:sensitive\",\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"Hashtag\":\"as:Hashtag\",\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"toot\":\"http://joinmastodon.org/ns#\",\"Emoji\":\"toot:Emoji\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"},\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\"}],\"id\":\"https://social.tcit.fr/users/tcit/statuses/101160195754333819\",\"type\":\"Note\",\"summary\":null,\"inReplyTo\":\"https://social.tcit.fr/users/tcit/statuses/101159468934977010\",\"published\":\"2018-11-30T12:48:08Z\",\"url\":\"https://social.tcit.fr/@tcit/101160195754333819\",\"attributedTo\":\"https://social.tcit.fr/users/tcit\",\"to\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"cc\":[\"https://social.tcit.fr/users/tcit/followers\"],\"sensitive\":false,\"atomUri\":\"https://social.tcit.fr/users/tcit/statuses/101160195754333819\",\"inReplyToAtomUri\":\"https://social.tcit.fr/users/tcit/statuses/101159468934977010\",\"conversation\":\"tag:social.tcit.fr,2018-11-30:objectId=3642669:objectType=Conversation\",\"content\":\"\\u003cp\\u003eOkay so YOLO.\\u003c/p\\u003e\",\"contentMap\":{\"fr\":\"\\u003cp\\u003eOkay so YOLO.\\u003c/p\\u003e\"},\"attachment\":[{\"type\":\"Document\",\"mediaType\":\"image/png\",\"url\":\"https://media.social.tcit.fr/mastodontcit/media_attachments/files/000/718/393/original/b56706a78fd355b8.png\",\"name\":\"Start of a 'group' URI RFC\"}],\"tag\":[]}",
"headers": {
"Date": "Tue, 04 Dec 2018 13:59:58 GMT",
"Content-Type": "application/activity+json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Link": "<https://social.tcit.fr/users/tcit/updates/15967.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://social.tcit.fr/users/tcit/statuses/101160195754333819>; rel=\"alternate\"; type=\"application/activity+json\"",
"Vary": "Accept,Accept-Encoding",
"Cache-Control": "max-age=180, public",
"ETag": "W/\"e878d9ab8dfa31073b27b4661046b911\"",
"X-Request-Id": "b598d538-88b5-4d7a-867c-d78b85ee5677",
"X-Runtime": "0.078823",
"X-Cached": "MISS"
},
"status_code": 200,
"type": "ok"
}
},
{
"request": {
"body": "",
"headers": {
"Accept": "application/activity+json"
},
"method": "get",
"options": {
"follow_redirect": "true",
"recv_timeout": 20000,
"connect_timeout": 10000
},
"request_body": "",
"url": "https://social.tcit.fr/users/tcit/statuses/101159468934977010"
},
"response": {
"binary": false,
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"sensitive\":\"as:sensitive\",\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"Hashtag\":\"as:Hashtag\",\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"toot\":\"http://joinmastodon.org/ns#\",\"Emoji\":\"toot:Emoji\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"},\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\"}],\"id\":\"https://social.tcit.fr/users/tcit/statuses/101159468934977010\",\"type\":\"Note\",\"summary\":null,\"inReplyTo\":null,\"published\":\"2018-11-30T09:43:18Z\",\"url\":\"https://social.tcit.fr/@tcit/101159468934977010\",\"attributedTo\":\"https://social.tcit.fr/users/tcit\",\"to\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"cc\":[\"https://social.tcit.fr/users/tcit/followers\"],\"sensitive\":false,\"atomUri\":\"https://social.tcit.fr/users/tcit/statuses/101159468934977010\",\"inReplyToAtomUri\":null,\"conversation\":\"tag:social.tcit.fr,2018-11-30:objectId=3642669:objectType=Conversation\",\"content\":\"\\u003cp\\u003eApart from PeerTube, which software that implements ActivityPub does have a group functionnality?\\u003cbr /\\u003eIt\\u0026apos;s to discuss about a Webfinger group: query prefix, similar to the acct: query prefix.\\u003c/p\\u003e\",\"contentMap\":{\"en\":\"\\u003cp\\u003eApart from PeerTube, which software that implements ActivityPub does have a group functionnality?\\u003cbr /\\u003eIt\\u0026apos;s to discuss about a Webfinger group: query prefix, similar to the acct: query prefix.\\u003c/p\\u003e\"},\"attachment\":[],\"tag\":[]}",
"headers": {
"Date": "Tue, 04 Dec 2018 13:59:58 GMT",
"Content-Type": "application/activity+json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Link": "<https://social.tcit.fr/users/tcit/updates/15941.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://social.tcit.fr/users/tcit/statuses/101159468934977010>; rel=\"alternate\"; type=\"application/activity+json\"",
"Vary": "Accept,Accept-Encoding",
"Cache-Control": "max-age=180, public",
"ETag": "W/\"a29cc605a433ed904736da57572038d3\"",
"X-Request-Id": "18488387-c5a3-40db-8c9e-5a3a067401a9",
"X-Runtime": "0.054993",
"X-Cached": "MISS"
},
"status_code": 200,
"type": "ok"
}
}
]

View File

@ -1,78 +0,0 @@
[
{
"request": {
"body": "",
"headers": {
"Accept": "application/activity+json"
},
"method": "get",
"options": {
"follow_redirect": "true",
"recv_timeout": 20000,
"connect_timeout": 10000
},
"request_body": "",
"url": "https://social.tcit.fr/users/tcit/statuses/99908779444618462"
},
"response": {
"binary": false,
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"sensitive\":\"as:sensitive\",\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"Hashtag\":\"as:Hashtag\",\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"toot\":\"http://joinmastodon.org/ns#\",\"Emoji\":\"toot:Emoji\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"},\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\"}],\"id\":\"https://social.tcit.fr/users/tcit/statuses/99908779444618462\",\"type\":\"Note\",\"summary\":null,\"inReplyTo\":null,\"published\":\"2018-04-23T12:36:31Z\",\"url\":\"https://social.tcit.fr/@tcit/99908779444618462\",\"attributedTo\":\"https://social.tcit.fr/users/tcit\",\"to\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"cc\":[\"https://social.tcit.fr/users/tcit/followers\"],\"sensitive\":false,\"atomUri\":\"https://social.tcit.fr/users/tcit/statuses/99908779444618462\",\"inReplyToAtomUri\":null,\"conversation\":\"tag:social.tcit.fr,2018-04-23:objectId=1769180:objectType=Conversation\",\"content\":\"\\u003cp\\u003eRimini - Les Wampas\\u003cbr /\\u003e\\u003ca href=\\\"https://combine.fm/spotify/track/5xo1GjsebrOd1iUVoJ6SEK\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"ellipsis\\\"\\u003ecombine.fm/spotify/track/5xo1G\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ejsebrOd1iUVoJ6SEK\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\",\"contentMap\":{\"fr\":\"\\u003cp\\u003eRimini - Les Wampas\\u003cbr /\\u003e\\u003ca href=\\\"https://combine.fm/spotify/track/5xo1GjsebrOd1iUVoJ6SEK\\\" rel=\\\"nofollow noopener\\\" target=\\\"_blank\\\"\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\\\"ellipsis\\\"\\u003ecombine.fm/spotify/track/5xo1G\\u003c/span\\u003e\\u003cspan class=\\\"invisible\\\"\\u003ejsebrOd1iUVoJ6SEK\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/p\\u003e\"},\"attachment\":[],\"tag\":[]}",
"headers": {
"Date": "Tue, 13 Nov 2018 11:02:32 GMT",
"Content-Type": "application/activity+json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Link": "<https://social.tcit.fr/users/tcit/updates/9225.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://social.tcit.fr/users/tcit/statuses/99908779444618462>; rel=\"alternate\"; type=\"application/activity+json\"",
"Vary": "Accept,Accept-Encoding",
"Cache-Control": "max-age=180, public",
"ETag": "W/\"4f1620f67825ded8c3ebde01dc48e44f\"",
"X-Request-Id": "6e8e4d12-8396-445e-909c-81dab9797449",
"X-Runtime": "0.057592",
"X-Cached": "MISS"
},
"status_code": 200,
"type": "ok"
}
},
{
"request": {
"body": "",
"headers": {
"Accept": "application/activity+json"
},
"method": "get",
"options": {
"follow_redirect": "true"
},
"request_body": "",
"url": "https://social.tcit.fr/users/tcit"
},
"response": {
"binary": false,
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"sensitive\":\"as:sensitive\",\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"Hashtag\":\"as:Hashtag\",\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"toot\":\"http://joinmastodon.org/ns#\",\"Emoji\":\"toot:Emoji\",\"focalPoint\":{\"@container\":\"@list\",\"@id\":\"toot:focalPoint\"},\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\"}],\"id\":\"https://social.tcit.fr/users/tcit\",\"type\":\"Person\",\"following\":\"https://social.tcit.fr/users/tcit/following\",\"followers\":\"https://social.tcit.fr/users/tcit/followers\",\"inbox\":\"https://social.tcit.fr/users/tcit/inbox\",\"outbox\":\"https://social.tcit.fr/users/tcit/outbox\",\"featured\":\"https://social.tcit.fr/users/tcit/collections/featured\",\"preferredUsername\":\"tcit\",\"name\":\"🦄 Thomas Citharel\",\"summary\":\"\\u003cp\\u003eHoping to make people\\u0026apos;s life better with free software at \\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e.\\u003c/p\\u003e\",\"url\":\"https://social.tcit.fr/@tcit\",\"manuallyApprovesFollowers\":false,\"publicKey\":{\"id\":\"https://social.tcit.fr/users/tcit#main-key\",\"owner\":\"https://social.tcit.fr/users/tcit\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApXwYMUdFg3XUd+bGsh8C\\nyiMRGpRGAWuCdM5pDWx5uM4pW2pM3xbHbcI21j9h8BmlAiPg6hbZD73KGly2N8Rt\\n5iIS0I+l6i8kA1JCCdlAaDTRd41RKMggZDoQvjVZQtsyE1VzMeU2kbqqTFN6ew7H\\nvbd6O0NhixoKoZ5f3jwuBDZoT0p1TAcaMdmG8oqHD97isizkDnRn8cOBA6wtI+xb\\n5xP2zxZMsLpTDZLiKU8XcPKZCw4OfQfmDmKkHtrFb77jCAQj/s/FxjVnvxRwmfhN\\nnWy0D+LUV/g63nHh/b5zXIeV92QZLvDYbgbezmzUzv9UeA1s70GGbaDqCIy85gw9\\n+wIDAQAB\\n-----END PUBLIC KEY-----\\n\"},\"tag\":[],\"attachment\":[{\"type\":\"PropertyValue\",\"name\":\"Works at\",\"value\":\"\\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@Framasoft\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003eFramasoft\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Pronouns\",\"value\":\"He/Him\"},{\"type\":\"PropertyValue\",\"name\":\"Work Account\",\"value\":\"\\u003cspan class=\\\"h-card\\\"\\u003e\\u003ca href=\\\"https://framapiaf.org/@tcit\\\" class=\\\"u-url mention\\\"\\u003e@\\u003cspan\\u003etcit\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/span\\u003e\"},{\"type\":\"PropertyValue\",\"name\":\"Pixelfed Account\",\"value\":\"@tcit@pix.tcit.fr\"}],\"endpoints\":{\"sharedInbox\":\"https://social.tcit.fr/inbox\"},\"icon\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://media.social.tcit.fr/mastodontcit/accounts/avatars/000/000/001/original/a28c50ce5f2b13fd.jpg\"},\"image\":{\"type\":\"Image\",\"mediaType\":\"image/jpeg\",\"url\":\"https://media.social.tcit.fr/mastodontcit/accounts/headers/000/000/001/original/4d1ab77c20265ee9.jpg\"}}",
"headers": {
"Date": "Tue, 13 Nov 2018 11:02:32 GMT",
"Content-Type": "application/activity+json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Link": "<https://social.tcit.fr/.well-known/webfinger?resource=acct%3Atcit%40social.tcit.fr>; rel=\"lrdd\"; type=\"application/xrd+xml\", <https://social.tcit.fr/users/tcit.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://social.tcit.fr/users/tcit>; rel=\"alternate\"; type=\"application/activity+json\"",
"Vary": "Accept,Accept-Encoding",
"Cache-Control": "max-age=180, public",
"ETag": "W/\"928f8a090d8c180ccc82fc1699f6c2a5\"",
"X-Request-Id": "9520c2ef-0089-4fb8-a6b4-95f3e217487c",
"X-Runtime": "0.043715",
"X-Cached": "MISS"
},
"status_code": 200,
"type": "ok"
}
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@ defmodule Mobilizon.ActorsTest do
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member, Follower, Bot}
alias Mobilizon.Users
alias Mobilizon.Media.File
import Mobilizon.Factory
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
@ -91,13 +92,23 @@ defmodule Mobilizon.ActorsTest do
test "get_actor_by_name/1 returns a remote actor" do
use_cassette "actors/remote_actor_mastodon_tcit" do
with {:ok,
%Actor{id: actor_id, preferred_username: preferred_username, domain: domain} =
_actor} <- Actors.get_or_fetch_by_url(@remote_account_url),
%Actor{id: actor_found_id} <-
Actors.get_actor_by_name("#{preferred_username}@#{domain}").id do
assert actor_found_id == actor_id
end
{:ok,
%Actor{
id: actor_id,
preferred_username: preferred_username,
domain: domain,
avatar: %File{name: picture_name} = _picture
} = _actor} = Actors.get_or_fetch_by_url(@remote_account_url)
assert picture_name == "avatar"
%Actor{
id: actor_found_id,
avatar: %File{name: picture_name} = _picture
} = Actors.get_actor_by_name("#{preferred_username}@#{domain}")
assert actor_found_id == actor_id
assert picture_name == "avatar"
end
end

View File

@ -0,0 +1,50 @@
defmodule Mobilizon.MediaTest do
use Mobilizon.DataCase
alias Mobilizon.Media
import Mobilizon.Factory
describe "media" do
alias Mobilizon.Media.Picture
@valid_attrs %{
file: %{
url: "https://something.tld/media/something",
name: "something old"
}
}
@update_attrs %{
file: %{
url: "https://something.tld/media/something_updated",
name: "something new"
}
}
test "get_picture!/1 returns the picture with given id" do
picture = insert(:picture)
assert Media.get_picture!(picture.id) == picture
end
test "create_picture/1 with valid data creates a picture" do
assert {:ok, %Picture{} = picture} = Media.create_picture(@valid_attrs)
assert picture.file.name == "something old"
end
test "update_picture/2 with valid data updates the picture" do
picture = insert(:picture)
assert {:ok, %Picture{} = picture} = Media.update_picture(picture, @update_attrs)
assert picture.file.name == "something new"
end
test "delete_picture/1 deletes the picture" do
picture = insert(:picture)
assert {:ok, %Picture{}} = Media.delete_picture(picture)
assert_raise Ecto.NoResultsError, fn -> Media.get_picture!(picture.id) end
end
test "change_picture/1 returns a picture changeset" do
picture = insert(:picture)
assert %Ecto.Changeset{} = Media.change_picture(picture)
end
end
end

View File

@ -72,15 +72,15 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
describe "fetching an" do
test "object by url" do
use_cassette "activity_pub/fetch_social_tcit_fr_status" do
use_cassette "activity_pub/fetch_framapiaf_framasoft_status" do
{:ok, object} =
ActivityPub.fetch_object_from_url(
"https://social.tcit.fr/users/tcit/statuses/99908779444618462"
"https://framapiaf.org/users/Framasoft/statuses/102093631881522097"
)
{:ok, object_again} =
ActivityPub.fetch_object_from_url(
"https://social.tcit.fr/users/tcit/statuses/99908779444618462"
"https://framapiaf.org/users/Framasoft/statuses/102093631881522097"
)
assert object.id == object_again.id
@ -88,14 +88,12 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
end
test "object reply by url" do
use_cassette "activity_pub/fetch_social_tcit_fr_reply" do
use_cassette "activity_pub/fetch_framasoft_framapiaf_reply" do
{:ok, object} =
ActivityPub.fetch_object_from_url(
"https://social.tcit.fr/users/tcit/statuses/101160654038714030"
)
ActivityPub.fetch_object_from_url("https://mamot.fr/@imacrea/102094441327423790")
assert object.in_reply_to_comment.url ==
"https://social.tcit.fr/users/tcit/statuses/101160195754333819"
"https://framapiaf.org/users/Framasoft/statuses/102093632302210150"
end
end
@ -103,7 +101,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
use_cassette "activity_pub/fetch_reply_to_framatube" do
{:ok, object} =
ActivityPub.fetch_object_from_url(
"https://framapiaf.org/@troisiemelobe/101156292125317651"
"https://diaspodon.fr/users/dada/statuses/100820008426311925"
)
assert object.in_reply_to_comment == nil

Some files were not shown because too many files have changed in this diff Show More