Refactor media upload

Use Upload Media logic from Pleroma

Backend changes for picture upload

Move AS <-> Model conversion to separate module

Front changes

Downgrade apollo-client: https://github.com/Akryum/vue-apollo/issues/577

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2019-05-22 14:12:11 +02:00
parent 9724bc8e9f
commit f90089e1bf
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
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)