Merge branch 'detect-images-in-body' into 'master'
Track usage of media files and add a job to clean them See merge request framasoft/mobilizon!727
This commit is contained in:
commit
ab153599e2
24
CHANGELOG.md
24
CHANGELOG.md
@ -6,6 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
|
||||
**This release adds new migrations, be sure to run them before restarting Mobilizon**
|
||||
|
||||
**This release has a repair step, be sure to run the command right after restarting Mobilizon**
|
||||
|
||||
### Special operations
|
||||
|
||||
* **Reattach media files to their entity.**
|
||||
When media files were uploaded and added in events and posts bodies, they were only attached to the profile that uploaded them, not to the event or post. This task attaches them back to their entity so that the command to clean orphan media files doesn't remove them.
|
||||
|
||||
* Source install
|
||||
`MIX_ENV=prod mix mobilizon.maintenance.fix_unattached_media_in_body`
|
||||
* Docker
|
||||
`docker-compose exec mobilizon mobilizon_ctl maintenance.fix_unattached_media_in_body`
|
||||
|
||||
### Added
|
||||
|
||||
- **Add a command to clean orphan media files**. There's a `--dry-run` option to see what files would have been deleted.
|
||||
**Make sure all media files have been reattached properly (see above) before running this command.**
|
||||
In 1.1.0 a scheduled job will be enabled to clear orphan media files automatically after a while.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix inline media that weren't being tracked, so that they are not considered orphans media files.
|
||||
|
||||
## 1.0.2 - 2020-11-15
|
||||
|
||||
**This release adds new migrations, be sure to run them before restarting Mobilizon**
|
||||
|
@ -28,6 +28,8 @@ config :mobilizon, :instance,
|
||||
upload_limit: 10_000_000,
|
||||
avatar_upload_limit: 2_000_000,
|
||||
banner_upload_limit: 4_000_000,
|
||||
remove_orphan_uploads: true,
|
||||
orphan_upload_grace_period_hours: 48,
|
||||
email_from: "noreply@localhost",
|
||||
email_reply_to: "noreply@localhost"
|
||||
|
||||
@ -250,6 +252,8 @@ config :mobilizon, Oban,
|
||||
crontab: [
|
||||
{"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background},
|
||||
{"17 * * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background}
|
||||
# To be activated in Mobilizon 1.2
|
||||
# {"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background}
|
||||
]
|
||||
|
||||
config :mobilizon, :rich_media,
|
||||
|
@ -212,7 +212,7 @@ import { SEARCH_PERSONS } from "../graphql/search";
|
||||
import { Actor, IActor, IPerson } from "../types/actor";
|
||||
import Image from "./Editor/Image";
|
||||
import MaxSize from "./Editor/MaxSize";
|
||||
import { UPLOAD_PICTURE } from "../graphql/upload";
|
||||
import { UPLOAD_MEDIA } from "../graphql/upload";
|
||||
import { listenFileUpload } from "../utils/upload";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
|
||||
import { IComment } from "../types/comment.model";
|
||||
@ -395,7 +395,15 @@ export default class EditorComponent extends Vue {
|
||||
new Image(),
|
||||
new MaxSize({ maxSize: this.maxSize }),
|
||||
],
|
||||
onUpdate: ({ getHTML }: { getHTML: Function }) => {
|
||||
onUpdate: ({
|
||||
getHTML,
|
||||
transaction,
|
||||
getJSON,
|
||||
}: {
|
||||
getHTML: Function;
|
||||
getJSON: Function;
|
||||
transaction: unknown;
|
||||
}) => {
|
||||
this.$emit("input", getHTML());
|
||||
},
|
||||
});
|
||||
@ -526,14 +534,14 @@ export default class EditorComponent extends Vue {
|
||||
const image = await listenFileUpload();
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: UPLOAD_PICTURE,
|
||||
mutation: UPLOAD_MEDIA,
|
||||
variables: {
|
||||
file: image,
|
||||
name: image.name,
|
||||
},
|
||||
});
|
||||
if (data.uploadPicture && data.uploadPicture.url) {
|
||||
command({ src: data.uploadPicture.url });
|
||||
if (data.uploadMedia && data.uploadMedia.url) {
|
||||
command({ src: data.uploadMedia.url, "data-media-id": data.uploadMedia.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -1,6 +1,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import { Node } from "tiptap";
|
||||
import { UPLOAD_PICTURE } from "@/graphql/upload";
|
||||
import { UPLOAD_MEDIA } from "@/graphql/upload";
|
||||
import apolloProvider from "@/vue-apollo";
|
||||
import ApolloClient from "apollo-client";
|
||||
import { NormalizedCacheObject } from "apollo-cache-inmemory";
|
||||
@ -27,16 +28,18 @@ export default class Image extends Node {
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
"data-media-id": {},
|
||||
},
|
||||
group: "inline",
|
||||
draggable: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: "img[src]",
|
||||
tag: "img",
|
||||
getAttrs: (dom: any) => ({
|
||||
src: dom.getAttribute("src"),
|
||||
title: dom.getAttribute("title"),
|
||||
alt: dom.getAttribute("alt"),
|
||||
"data-media-id": dom.getAttribute("data-media-id"),
|
||||
}),
|
||||
},
|
||||
],
|
||||
@ -92,13 +95,16 @@ export default class Image extends Node {
|
||||
try {
|
||||
images.forEach(async (image) => {
|
||||
const { data } = await client.mutate({
|
||||
mutation: UPLOAD_PICTURE,
|
||||
mutation: UPLOAD_MEDIA,
|
||||
variables: {
|
||||
file: image,
|
||||
name: image.name,
|
||||
},
|
||||
});
|
||||
const node = schema.nodes.image.create({ src: data.uploadPicture.url });
|
||||
const node = schema.nodes.image.create({
|
||||
src: data.uploadMedia.url,
|
||||
"data-media-id": data.uploadMedia.id,
|
||||
});
|
||||
const transaction = view.state.tr.insert(coordinates.pos, node);
|
||||
view.dispatch(transaction);
|
||||
});
|
||||
|
@ -60,14 +60,14 @@ figure.image {
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { IPicture } from "@/types/picture.model";
|
||||
import { IMedia } from "@/types/media.model";
|
||||
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class PictureUpload extends Vue {
|
||||
@Model("change", { type: File }) readonly pictureFile!: File;
|
||||
|
||||
@Prop({ type: Object, required: false }) defaultImage!: IPicture;
|
||||
@Prop({ type: Object, required: false }) defaultImage!: IMedia;
|
||||
|
||||
@Prop({ type: String, required: false, default: "image/gif,image/png,image/jpeg,image/webp" })
|
||||
accept!: string;
|
||||
@ -100,7 +100,7 @@ export default class PictureUpload extends Vue {
|
||||
}
|
||||
|
||||
@Watch("defaultImage")
|
||||
onDefaultImageChange(defaultImage: IPicture): void {
|
||||
onDefaultImageChange(defaultImage: IMedia): void {
|
||||
console.log("onDefaultImageChange", defaultImage);
|
||||
this.imageSrc = defaultImage ? defaultImage.url : null;
|
||||
}
|
||||
|
@ -421,7 +421,7 @@ export const CREATE_PERSON = gql`
|
||||
$preferredUsername: String!
|
||||
$name: String!
|
||||
$summary: String
|
||||
$avatar: PictureInput
|
||||
$avatar: MediaInput
|
||||
) {
|
||||
createPerson(
|
||||
preferredUsername: $preferredUsername
|
||||
@ -442,7 +442,7 @@ export const CREATE_PERSON = gql`
|
||||
`;
|
||||
|
||||
export const UPDATE_PERSON = gql`
|
||||
mutation UpdatePerson($id: ID!, $name: String, $summary: String, $avatar: PictureInput) {
|
||||
mutation UpdatePerson($id: ID!, $name: String, $summary: String, $avatar: MediaInput) {
|
||||
updatePerson(id: $id, name: $name, summary: $summary, avatar: $avatar) {
|
||||
id
|
||||
preferredUsername
|
||||
|
@ -244,7 +244,7 @@ export const CREATE_EVENT = gql`
|
||||
$joinOptions: EventJoinOptions,
|
||||
$draft: Boolean,
|
||||
$tags: [String],
|
||||
$picture: PictureInput,
|
||||
$picture: MediaInput,
|
||||
$onlineAddress: String,
|
||||
$phoneAddress: String,
|
||||
$category: String,
|
||||
@ -355,7 +355,7 @@ export const EDIT_EVENT = gql`
|
||||
$joinOptions: EventJoinOptions,
|
||||
$draft: Boolean,
|
||||
$tags: [String],
|
||||
$picture: PictureInput,
|
||||
$picture: MediaInput,
|
||||
$onlineAddress: String,
|
||||
$phoneAddress: String,
|
||||
$organizerActorId: ID,
|
||||
|
@ -227,8 +227,8 @@ export const CREATE_GROUP = gql`
|
||||
$preferredUsername: String!
|
||||
$name: String!
|
||||
$summary: String
|
||||
$avatar: PictureInput
|
||||
$banner: PictureInput
|
||||
$avatar: MediaInput
|
||||
$banner: MediaInput
|
||||
) {
|
||||
createGroup(
|
||||
preferredUsername: $preferredUsername
|
||||
@ -259,8 +259,8 @@ export const UPDATE_GROUP = gql`
|
||||
$id: ID!
|
||||
$name: String
|
||||
$summary: String
|
||||
$avatar: PictureInput
|
||||
$banner: PictureInput
|
||||
$avatar: MediaInput
|
||||
$banner: MediaInput
|
||||
$visibility: GroupVisibility
|
||||
$openness: Openness
|
||||
$physicalAddress: AddressInput
|
||||
|
@ -119,7 +119,7 @@ export const CREATE_POST = gql`
|
||||
$visibility: PostVisibility
|
||||
$draft: Boolean
|
||||
$tags: [String]
|
||||
$picture: PictureInput
|
||||
$picture: MediaInput
|
||||
) {
|
||||
createPost(
|
||||
title: $title
|
||||
@ -145,7 +145,7 @@ export const UPDATE_POST = gql`
|
||||
$visibility: PostVisibility
|
||||
$draft: Boolean
|
||||
$tags: [String]
|
||||
$picture: PictureInput
|
||||
$picture: MediaInput
|
||||
) {
|
||||
updatePost(
|
||||
id: $id
|
||||
|
@ -1,17 +1,17 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const UPLOAD_PICTURE = gql`
|
||||
mutation UploadPicture($file: Upload!, $alt: String, $name: String!) {
|
||||
uploadPicture(file: $file, alt: $alt, name: $name) {
|
||||
export const UPLOAD_MEDIA = gql`
|
||||
mutation UploadMedia($file: Upload!, $alt: String, $name: String!) {
|
||||
uploadMedia(file: $file, alt: $alt, name: $name) {
|
||||
url
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const REMOVE_PICTURE = gql`
|
||||
mutation RemovePicture($id: ID!) {
|
||||
removePicture(id: $id) {
|
||||
export const REMOVE_MEDIA = gql`
|
||||
mutation RemoveMedia($id: ID!) {
|
||||
removeMedia(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IPicture } from "@/types/picture.model";
|
||||
import { IMedia } from "@/types/media.model";
|
||||
|
||||
export enum ActorType {
|
||||
PERSON = "PERSON",
|
||||
@ -17,17 +17,17 @@ export interface IActor {
|
||||
summary: string;
|
||||
preferredUsername: string;
|
||||
suspended: boolean;
|
||||
avatar?: IPicture | null;
|
||||
banner?: IPicture | null;
|
||||
avatar?: IMedia | null;
|
||||
banner?: IMedia | null;
|
||||
type: ActorType;
|
||||
}
|
||||
|
||||
export class Actor implements IActor {
|
||||
id?: string;
|
||||
|
||||
avatar: IPicture | null = null;
|
||||
avatar: IMedia | null = null;
|
||||
|
||||
banner: IPicture | null = null;
|
||||
banner: IMedia | null = null;
|
||||
|
||||
domain: string | null = null;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Address, IAddress } from "@/types/address.model";
|
||||
import { ITag } from "@/types/tag.model";
|
||||
import { IPicture } from "@/types/picture.model";
|
||||
import { IMedia } from "@/types/media.model";
|
||||
import { IComment } from "@/types/comment.model";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { Actor, Group, IActor, IGroup, IPerson } from "./actor";
|
||||
@ -69,7 +69,7 @@ interface IEventEditJSON {
|
||||
visibility: EventVisibility;
|
||||
joinOptions: EventJoinOptions;
|
||||
draft: boolean;
|
||||
picture?: IPicture | { pictureId: string } | null;
|
||||
picture?: IMedia | { mediaId: string } | null;
|
||||
attributedToId: string | null;
|
||||
onlineAddress?: string;
|
||||
phoneAddress?: string;
|
||||
@ -96,7 +96,7 @@ export interface IEvent {
|
||||
joinOptions: EventJoinOptions;
|
||||
draft: boolean;
|
||||
|
||||
picture: IPicture | null;
|
||||
picture: IMedia | null;
|
||||
|
||||
organizerActor?: IActor;
|
||||
attributedTo?: IGroup;
|
||||
@ -142,7 +142,7 @@ export class EventModel implements IEvent {
|
||||
|
||||
physicalAddress?: IAddress;
|
||||
|
||||
picture: IPicture | null = null;
|
||||
picture: IMedia | null = null;
|
||||
|
||||
visibility = EventVisibility.PUBLIC;
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
export interface IPicture {
|
||||
export interface IMedia {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export interface IPictureUpload {
|
||||
export interface IMediaUpload {
|
||||
file: File;
|
||||
name: string;
|
||||
alt: string | null;
|
@ -1,5 +1,5 @@
|
||||
import { ITag } from "./tag.model";
|
||||
import { IPicture } from "./picture.model";
|
||||
import { IMedia } from "./media.model";
|
||||
import { IActor } from "./actor";
|
||||
|
||||
export enum PostVisibility {
|
||||
@ -17,7 +17,7 @@ export interface IPost {
|
||||
title: string;
|
||||
body: string;
|
||||
tags?: ITag[];
|
||||
picture?: IPicture | null;
|
||||
picture?: IMedia | null;
|
||||
draft: boolean;
|
||||
visibility: PostVisibility;
|
||||
author?: IActor;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IPicture } from "@/types/picture.model";
|
||||
import { IMedia } from "@/types/media.model";
|
||||
|
||||
export async function buildFileFromIPicture(obj: IPicture | null | undefined): Promise<File | null> {
|
||||
export async function buildFileFromIMedia(obj: IMedia | null | undefined): Promise<File | null> {
|
||||
if (!obj) return Promise.resolve(null);
|
||||
|
||||
const response = await fetch(obj.url);
|
||||
@ -14,7 +14,7 @@ export function buildFileVariable(file: File | null, name: string, alt?: string)
|
||||
|
||||
return {
|
||||
[name]: {
|
||||
picture: {
|
||||
media: {
|
||||
name: file.name,
|
||||
alt: alt || file.name,
|
||||
file,
|
||||
|
@ -124,7 +124,6 @@ h1 {
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { IPicture } from "@/types/picture.model";
|
||||
import {
|
||||
CREATE_PERSON,
|
||||
CURRENT_ACTOR_CLIENT,
|
||||
@ -137,7 +136,7 @@ import { IPerson, Person } from "../../../types/actor";
|
||||
import PictureUpload from "../../../components/PictureUpload.vue";
|
||||
import { MOBILIZON_INSTANCE_HOST } from "../../../api/_entrypoint";
|
||||
import RouteName from "../../../router/name";
|
||||
import { buildFileFromIPicture, buildFileVariable } from "../../../utils/image";
|
||||
import { buildFileVariable } from "../../../utils/image";
|
||||
import { changeIdentity } from "../../../utils/auth";
|
||||
import identityEditionMixin from "../../../mixins/identityEdition";
|
||||
|
||||
|
@ -377,7 +377,7 @@ import {
|
||||
import { IPerson, Person, displayNameAndUsername } from "../../types/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { ITag } from "../../types/tag.model";
|
||||
import { buildFileFromIPicture, buildFileVariable, readFileAsync } from "../../utils/image";
|
||||
import { buildFileFromIMedia, buildFileVariable, readFileAsync } from "../../utils/image";
|
||||
import RouteName from "../../router/name";
|
||||
import "intersection-observer";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
@ -517,7 +517,7 @@ export default class EditEvent extends Vue {
|
||||
);
|
||||
this.observer.observe(this.$refs.bottomObserver as Element);
|
||||
|
||||
this.pictureFile = await buildFileFromIPicture(this.event.picture);
|
||||
this.pictureFile = await buildFileFromIMedia(this.event.picture);
|
||||
this.limitedPlaces = this.event.options.maximumAttendeeCapacity > 0;
|
||||
if (!(this.isUpdate || this.isDuplicate)) {
|
||||
this.initializeEvent();
|
||||
@ -775,11 +775,11 @@ export default class EditEvent extends Vue {
|
||||
|
||||
try {
|
||||
if (this.event.picture && this.pictureFile) {
|
||||
const oldPictureFile = (await buildFileFromIPicture(this.event.picture)) as File;
|
||||
const oldPictureFile = (await buildFileFromIMedia(this.event.picture)) as File;
|
||||
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
||||
const newPictureFileContent = await readFileAsync(this.pictureFile as File);
|
||||
if (oldPictureFileContent === newPictureFileContent) {
|
||||
res.picture = { pictureId: this.event.picture.id };
|
||||
res.picture = { mediaId: this.event.picture.id };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -246,7 +246,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
|
||||
if (this.avatarFile) {
|
||||
avatarObj = {
|
||||
avatar: {
|
||||
picture: {
|
||||
media: {
|
||||
name: this.avatarFile.name,
|
||||
alt: `${this.group.preferredUsername}'s avatar`,
|
||||
file: this.avatarFile,
|
||||
@ -258,7 +258,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
|
||||
if (this.bannerFile) {
|
||||
bannerObj = {
|
||||
banner: {
|
||||
picture: {
|
||||
media: {
|
||||
name: this.bannerFile.name,
|
||||
alt: `${this.group.preferredUsername}'s banner`,
|
||||
file: this.bannerFile,
|
||||
|
@ -103,7 +103,7 @@
|
||||
import { Component, Prop } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { FETCH_GROUP } from "@/graphql/group";
|
||||
import { buildFileFromIPicture, readFileAsync } from "@/utils/image";
|
||||
import { buildFileFromIMedia, readFileAsync } from "@/utils/image";
|
||||
import GroupMixin from "@/mixins/group";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
@ -188,7 +188,7 @@ export default class EditPost extends mixins(GroupMixin) {
|
||||
errors: Record<string, unknown> = {};
|
||||
|
||||
async mounted(): Promise<void> {
|
||||
this.pictureFile = await buildFileFromIPicture(this.post.picture);
|
||||
this.pictureFile = await buildFileFromIMedia(this.post.picture);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
@ -277,11 +277,11 @@ export default class EditPost extends mixins(GroupMixin) {
|
||||
}
|
||||
try {
|
||||
if (this.post.picture) {
|
||||
const oldPictureFile = (await buildFileFromIPicture(this.post.picture)) as File;
|
||||
const oldPictureFile = (await buildFileFromIMedia(this.post.picture)) as File;
|
||||
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
||||
const newPictureFileContent = await readFileAsync(this.pictureFile as File);
|
||||
if (oldPictureFileContent === newPictureFileContent) {
|
||||
obj.picture = { pictureId: this.post.picture.id };
|
||||
obj.picture = { mediaId: this.post.picture.id };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -10,7 +10,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Medias.Media
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
|
||||
@ -333,50 +333,50 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
Return AS Link data from
|
||||
|
||||
* a `Plug.Upload` struct, stored an returned
|
||||
* a `Picture`, directly returned
|
||||
* a map containing picture information, stored, saved and returned
|
||||
* a `Media`, directly returned
|
||||
* a map containing media information, stored, saved and returned
|
||||
|
||||
Save picture data from %Plug.Upload{} and return AS Link data.
|
||||
Save media data from %Plug.Upload{} and return AS Link data.
|
||||
"""
|
||||
def make_picture_data(%Plug.Upload{} = picture, opts) do
|
||||
case Mobilizon.Web.Upload.store(picture, opts) do
|
||||
{:ok, picture} ->
|
||||
picture
|
||||
def make_media_data(%Plug.Upload{} = media, opts) do
|
||||
case Mobilizon.Web.Upload.store(media, opts) do
|
||||
{:ok, media} ->
|
||||
media
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def make_picture_data(%Picture{} = picture) do
|
||||
Converter.Picture.model_to_as(picture)
|
||||
def make_media_data(%Media{} = media) do
|
||||
Converter.Media.model_to_as(media)
|
||||
end
|
||||
|
||||
def make_picture_data(picture) when is_map(picture) do
|
||||
def make_media_data(media) when is_map(media) do
|
||||
with {:ok, %{"url" => [%{"href" => url, "mediaType" => content_type}], "size" => size}} <-
|
||||
Mobilizon.Web.Upload.store(picture.file),
|
||||
{:picture_exists, nil} <- {:picture_exists, Mobilizon.Media.get_picture_by_url(url)},
|
||||
{:ok, %Picture{file: _file} = picture} <-
|
||||
Mobilizon.Media.create_picture(%{
|
||||
Mobilizon.Web.Upload.store(media.file),
|
||||
{:media_exists, nil} <- {:media_exists, Mobilizon.Medias.get_media_by_url(url)},
|
||||
{:ok, %Media{file: _file} = media} <-
|
||||
Mobilizon.Medias.create_media(%{
|
||||
"file" => %{
|
||||
"url" => url,
|
||||
"name" => picture.name,
|
||||
"name" => media.name,
|
||||
"content_type" => content_type,
|
||||
"size" => size
|
||||
},
|
||||
"actor_id" => picture.actor_id
|
||||
"actor_id" => media.actor_id
|
||||
}) do
|
||||
Converter.Picture.model_to_as(picture)
|
||||
Converter.Media.model_to_as(media)
|
||||
else
|
||||
{:picture_exists, %Picture{file: _file} = picture} ->
|
||||
Converter.Picture.model_to_as(picture)
|
||||
{:media_exists, %Media{file: _file} = media} ->
|
||||
Converter.Media.model_to_as(media)
|
||||
|
||||
err ->
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def make_picture_data(nil), do: nil
|
||||
def make_media_data(nil), do: nil
|
||||
|
||||
@doc """
|
||||
Make announce activity data for the given actor and object
|
||||
|
@ -10,11 +10,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events.Event, as: EventModel
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Medias.Media
|
||||
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Picture, as: PictureConverter
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Media, as: MediaConverter
|
||||
|
||||
import Mobilizon.Federation.ActivityStream.Converter.Utils,
|
||||
only: [
|
||||
@ -55,10 +55,10 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
|
||||
picture_id =
|
||||
with true <- length(attachments) > 0,
|
||||
{:ok, %Picture{id: picture_id}} <-
|
||||
{:ok, %Media{id: picture_id}} <-
|
||||
attachments
|
||||
|> hd()
|
||||
|> PictureConverter.find_or_create_picture(actor_id) do
|
||||
|> MediaConverter.find_or_create_media(actor_id) do
|
||||
picture_id
|
||||
else
|
||||
_err ->
|
||||
@ -239,7 +239,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
res,
|
||||
"attachment",
|
||||
[],
|
||||
&(&1 ++ [PictureConverter.model_to_as(event.picture)])
|
||||
&(&1 ++ [MediaConverter.model_to_as(event.picture)])
|
||||
)
|
||||
end
|
||||
|
||||
|
63
lib/federation/activity_stream/converter/media.ex
Normal file
63
lib/federation/activity_stream/converter/media.ex
Normal file
@ -0,0 +1,63 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
|
||||
@moduledoc """
|
||||
Media converter.
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Medias
|
||||
alias Mobilizon.Medias.Media, as: MediaModel
|
||||
|
||||
alias Mobilizon.Web.Upload
|
||||
|
||||
@http_options [
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@doc """
|
||||
Convert a media struct to an ActivityStream representation.
|
||||
"""
|
||||
@spec model_to_as(MediaModel.t()) :: map
|
||||
def model_to_as(%MediaModel{file: file}) do
|
||||
%{
|
||||
"type" => "Document",
|
||||
"mediaType" => file.content_type,
|
||||
"url" => file.url,
|
||||
"name" => file.name
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Save media data from raw data and return AS Link data.
|
||||
"""
|
||||
def find_or_create_media(%{"type" => "Link", "href" => url}, actor_id),
|
||||
do: find_or_create_media(url, actor_id)
|
||||
|
||||
def find_or_create_media(
|
||||
%{"type" => "Document", "url" => media_url, "name" => name},
|
||||
actor_id
|
||||
)
|
||||
when is_bitstring(media_url) do
|
||||
with {:ok, %{body: body}} <- Tesla.get(media_url, opts: @http_options),
|
||||
{:ok, %{name: name, url: url, content_type: content_type, size: size}} <-
|
||||
Upload.store(%{body: body, name: name}),
|
||||
{:media_exists, nil} <- {:media_exists, Medias.get_media_by_url(url)} do
|
||||
Medias.create_media(%{
|
||||
"file" => %{
|
||||
"url" => url,
|
||||
"name" => name,
|
||||
"content_type" => content_type,
|
||||
"size" => size
|
||||
},
|
||||
"actor_id" => actor_id
|
||||
})
|
||||
else
|
||||
{:media_exists, %MediaModel{file: _file} = media} ->
|
||||
{:ok, media}
|
||||
|
||||
err ->
|
||||
err
|
||||
end
|
||||
end
|
||||
end
|
@ -1,63 +0,0 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.Converter.Picture do
|
||||
@moduledoc """
|
||||
Picture converter.
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Media
|
||||
alias Mobilizon.Media.Picture, as: PictureModel
|
||||
|
||||
alias Mobilizon.Web.Upload
|
||||
|
||||
@http_options [
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@doc """
|
||||
Convert a picture struct to an ActivityStream representation.
|
||||
"""
|
||||
@spec model_to_as(PictureModel.t()) :: map
|
||||
def model_to_as(%PictureModel{file: file}) do
|
||||
%{
|
||||
"type" => "Document",
|
||||
"mediaType" => file.content_type,
|
||||
"url" => file.url,
|
||||
"name" => file.name
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Save picture data from raw data and return AS Link data.
|
||||
"""
|
||||
def find_or_create_picture(%{"type" => "Link", "href" => url}, actor_id),
|
||||
do: find_or_create_picture(url, actor_id)
|
||||
|
||||
def find_or_create_picture(
|
||||
%{"type" => "Document", "url" => picture_url, "name" => name},
|
||||
actor_id
|
||||
)
|
||||
when is_bitstring(picture_url) do
|
||||
with {:ok, %{body: body}} <- Tesla.get(picture_url, opts: @http_options),
|
||||
{:ok, %{name: name, url: url, content_type: content_type, size: size}} <-
|
||||
Upload.store(%{body: body, name: name}),
|
||||
{:picture_exists, nil} <- {:picture_exists, Media.get_picture_by_url(url)} do
|
||||
Media.create_picture(%{
|
||||
"file" => %{
|
||||
"url" => url,
|
||||
"name" => name,
|
||||
"content_type" => content_type,
|
||||
"size" => size
|
||||
},
|
||||
"actor_id" => actor_id
|
||||
})
|
||||
else
|
||||
{:picture_exists, %PictureModel{file: _file} = picture} ->
|
||||
{:ok, picture}
|
||||
|
||||
err ->
|
||||
err
|
||||
end
|
||||
end
|
||||
end
|
@ -1,34 +1,59 @@
|
||||
defmodule Mobilizon.GraphQL.API.Comments do
|
||||
@moduledoc """
|
||||
API for Comments.
|
||||
API for discussions and comments.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.GraphQL.API.Utils
|
||||
|
||||
@doc """
|
||||
Create a comment
|
||||
|
||||
Creates a comment from an actor
|
||||
"""
|
||||
@spec create_comment(map) :: {:ok, Activity.t(), Comment.t()} | any
|
||||
def create_comment(args) do
|
||||
args = extract_pictures_from_comment_body(args)
|
||||
ActivityPub.create(:comment, args, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a comment
|
||||
"""
|
||||
@spec update_comment(Comment.t(), map()) :: {:ok, Activity.t(), Comment.t()} | any
|
||||
def update_comment(%Comment{} = comment, args) do
|
||||
args = extract_pictures_from_comment_body(args)
|
||||
ActivityPub.update(comment, args, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a comment
|
||||
|
||||
Deletes a comment from an actor
|
||||
"""
|
||||
@spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any
|
||||
def delete_comment(%Comment{} = comment, %Actor{} = actor) do
|
||||
ActivityPub.delete(comment, actor, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a discussion (or reply to a discussion)
|
||||
"""
|
||||
@spec create_discussion(map()) :: map()
|
||||
def create_discussion(args) do
|
||||
args = extract_pictures_from_comment_body(args)
|
||||
|
||||
ActivityPub.create(
|
||||
:discussion,
|
||||
args,
|
||||
true
|
||||
)
|
||||
end
|
||||
|
||||
@spec extract_pictures_from_comment_body(map()) :: map()
|
||||
defp extract_pictures_from_comment_body(%{text: text, actor_id: actor_id} = args) do
|
||||
pictures = Utils.extract_pictures_from_body(text, actor_id)
|
||||
Map.put(args, :media, pictures)
|
||||
end
|
||||
|
||||
defp extract_pictures_from_comment_body(args), do: args
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
|
||||
@doc """
|
||||
Create an event
|
||||
@ -15,6 +16,7 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||
@spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any
|
||||
def create_event(args) do
|
||||
with organizer_actor <- Map.get(args, :organizer_actor),
|
||||
args <- extract_pictures_from_event_body(args, organizer_actor),
|
||||
args <-
|
||||
Map.update(args, :picture, nil, fn picture ->
|
||||
process_picture(picture, organizer_actor)
|
||||
@ -30,6 +32,7 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||
@spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any
|
||||
def update_event(args, %Event{} = event) do
|
||||
with organizer_actor <- Map.get(args, :organizer_actor),
|
||||
args <- extract_pictures_from_event_body(args, organizer_actor),
|
||||
args <-
|
||||
Map.update(args, :picture, nil, fn picture ->
|
||||
process_picture(picture, organizer_actor)
|
||||
@ -40,23 +43,32 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||
|
||||
@doc """
|
||||
Trigger the deletion of an event
|
||||
|
||||
If the event is deleted by
|
||||
"""
|
||||
def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do
|
||||
ActivityPub.delete(event, actor, federate)
|
||||
end
|
||||
|
||||
defp process_picture(nil, _), do: nil
|
||||
defp process_picture(%{picture_id: _picture_id} = args, _), do: args
|
||||
defp process_picture(%{media_id: _picture_id} = args, _), do: args
|
||||
|
||||
defp process_picture(%{picture: picture}, %Actor{id: actor_id}) do
|
||||
defp process_picture(%{media: media}, %Actor{id: actor_id}) do
|
||||
%{
|
||||
file:
|
||||
picture
|
||||
media
|
||||
|> Map.get(:file)
|
||||
|> Utils.make_picture_data(description: Map.get(picture, :name)),
|
||||
|> Utils.make_media_data(description: Map.get(media, :name)),
|
||||
actor_id: actor_id
|
||||
}
|
||||
end
|
||||
|
||||
@spec extract_pictures_from_event_body(map(), Actor.t()) :: map()
|
||||
defp extract_pictures_from_event_body(
|
||||
%{description: description} = args,
|
||||
%Actor{id: organizer_actor_id}
|
||||
) do
|
||||
pictures = APIUtils.extract_pictures_from_body(description, organizer_actor_id)
|
||||
Map.put(args, :media, pictures)
|
||||
end
|
||||
|
||||
defp extract_pictures_from_event_body(args, _), do: args
|
||||
end
|
||||
|
@ -3,7 +3,8 @@ defmodule Mobilizon.GraphQL.API.Utils do
|
||||
Utils for API.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.{Config, Medias}
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Service.Formatter
|
||||
|
||||
@doc """
|
||||
@ -40,4 +41,41 @@ defmodule Mobilizon.GraphQL.API.Utils do
|
||||
{:error, "Comment must be up to #{max_size} characters"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Use the data-media-id attributes to extract media from body text
|
||||
"""
|
||||
@spec extract_pictures_from_body(String.t(), integer() | String.t()) :: list(Media.t())
|
||||
def extract_pictures_from_body(body, actor_id) do
|
||||
body
|
||||
|> do_extract_pictures_from_body()
|
||||
|> Enum.map(&fetch_picture(&1, actor_id))
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
||||
@spec do_extract_pictures_from_body(String.t()) :: list(String.t())
|
||||
defp do_extract_pictures_from_body(body) when is_nil(body) or body == "", do: []
|
||||
|
||||
defp do_extract_pictures_from_body(body) do
|
||||
{:ok, document} = Floki.parse_document(body)
|
||||
|
||||
document
|
||||
|> Floki.attribute("img", "data-media-id")
|
||||
end
|
||||
|
||||
@spec fetch_picture(String.t() | integer(), String.t() | integer()) :: Media.t() | nil
|
||||
defp fetch_picture(id, actor_id) do
|
||||
with %Media{actor_id: media_actor_id} = media <- Medias.get_media(id),
|
||||
{:owns_media, true} <-
|
||||
{:owns_media, check_actor_owns_media?(actor_id, media_actor_id)} do
|
||||
media
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_actor_owns_media?(integer() | String.t(), integer() | String.t()) :: boolean()
|
||||
defp check_actor_owns_media?(actor_id, media_actor_id) do
|
||||
actor_id == media_actor_id || Mobilizon.Actors.is_member?(media_actor_id, actor_id)
|
||||
end
|
||||
end
|
||||
|
@ -7,6 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.GraphQL.API.Comments
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Web.Gettext
|
||||
@ -94,17 +95,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, group_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.create(
|
||||
:discussion,
|
||||
%{
|
||||
title: title,
|
||||
text: text,
|
||||
actor_id: group_id,
|
||||
creator_id: creator_id,
|
||||
attributed_to_id: group_id
|
||||
},
|
||||
true
|
||||
) do
|
||||
Comments.create_discussion(%{
|
||||
title: title,
|
||||
text: text,
|
||||
actor_id: group_id,
|
||||
creator_id: creator_id,
|
||||
attributed_to_id: group_id
|
||||
}) do
|
||||
{:ok, discussion}
|
||||
else
|
||||
{:member, false} ->
|
||||
@ -134,19 +131,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
||||
{:no_discussion, Discussions.get_discussion(discussion_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.create(
|
||||
:discussion,
|
||||
%{
|
||||
text: text,
|
||||
discussion_id: discussion_id,
|
||||
actor_id: creator_id,
|
||||
attributed_to_id: actor_id,
|
||||
in_reply_to_comment_id: last_comment_id,
|
||||
origin_comment_id:
|
||||
origin_comment_id || previous_in_reply_to_comment_id || last_comment_id
|
||||
},
|
||||
true
|
||||
) do
|
||||
Comments.create_discussion(%{
|
||||
text: text,
|
||||
discussion_id: discussion_id,
|
||||
actor_id: creator_id,
|
||||
attributed_to_id: actor_id,
|
||||
in_reply_to_comment_id: last_comment_id,
|
||||
origin_comment_id:
|
||||
origin_comment_id || previous_in_reply_to_comment_id || last_comment_id
|
||||
}) do
|
||||
{:ok, discussion}
|
||||
end
|
||||
end
|
||||
|
@ -96,8 +96,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
# TODO Move me to somewhere cleaner
|
||||
defp save_attached_pictures(args) do
|
||||
Enum.reduce([:avatar, :banner], args, fn key, args ->
|
||||
if Map.has_key?(args, key) && !is_nil(args[key][:picture]) do
|
||||
pic = args[key][:picture]
|
||||
if Map.has_key?(args, key) && !is_nil(args[key][:media]) do
|
||||
pic = args[key][:media]
|
||||
|
||||
with {:ok, %{name: name, url: url, content_type: content_type, size: _size}} <-
|
||||
Upload.store(pic.file, type: key, description: pic.alt) do
|
||||
|
@ -1,50 +1,47 @@
|
||||
defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||
defmodule Mobilizon.GraphQL.Resolvers.Media do
|
||||
@moduledoc """
|
||||
Handles the picture-related GraphQL calls
|
||||
Handles the media-related GraphQL calls
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.{Media, Users}
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.{Medias, Users}
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
@doc """
|
||||
Get picture for an event
|
||||
Get media for an event
|
||||
|
||||
See Mobilizon.Web.Resolvers.Event.create_event/3
|
||||
"""
|
||||
def picture(%{picture_id: picture_id} = _parent, _args, _resolution) do
|
||||
with {:ok, picture} <- do_fetch_picture(picture_id), do: {:ok, picture}
|
||||
def media(%{picture_id: media_id} = _parent, _args, _resolution) do
|
||||
with {:ok, media} <- do_fetch_media(media_id), do: {:ok, media}
|
||||
end
|
||||
|
||||
def picture(%{picture: picture} = _parent, _args, _resolution), do: {:ok, picture}
|
||||
def picture(_parent, %{id: picture_id}, _resolution), do: do_fetch_picture(picture_id)
|
||||
def picture(_parent, _args, _resolution), do: {:ok, nil}
|
||||
def media(%{picture: media} = _parent, _args, _resolution), do: {:ok, media}
|
||||
def media(_parent, %{id: media_id}, _resolution), do: do_fetch_media(media_id)
|
||||
def media(_parent, _args, _resolution), do: {:ok, nil}
|
||||
|
||||
@spec do_fetch_picture(nil) :: {:error, nil}
|
||||
defp do_fetch_picture(nil), do: {:error, nil}
|
||||
def medias(%{media: medias}, _args, _resolution) do
|
||||
{:ok, Enum.map(medias, &transform_media/1)}
|
||||
end
|
||||
|
||||
@spec do_fetch_picture(String.t()) :: {:ok, Picture.t()} | {:error, :not_found}
|
||||
defp do_fetch_picture(picture_id) do
|
||||
case Media.get_picture(picture_id) do
|
||||
%Picture{id: id, file: file} ->
|
||||
{:ok,
|
||||
%{
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
id: id,
|
||||
content_type: file.content_type,
|
||||
size: file.size
|
||||
}}
|
||||
@spec do_fetch_media(nil) :: {:error, nil}
|
||||
defp do_fetch_media(nil), do: {:error, nil}
|
||||
|
||||
@spec do_fetch_media(String.t()) :: {:ok, Media.t()} | {:error, :not_found}
|
||||
defp do_fetch_media(media_id) do
|
||||
case Medias.get_media(media_id) do
|
||||
%Media{} = media ->
|
||||
{:ok, transform_media(media)}
|
||||
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@spec upload_picture(map, map, map) :: {:ok, Picture.t()} | {:error, any}
|
||||
def upload_picture(
|
||||
@spec upload_media(map, map, map) :: {:ok, Media.t()} | {:error, any}
|
||||
def upload_media(
|
||||
_parent,
|
||||
%{file: %Plug.Upload{} = file} = args,
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
@ -57,16 +54,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||
|> Map.put(:url, url)
|
||||
|> Map.put(:size, size)
|
||||
|> Map.put(:content_type, content_type),
|
||||
{:ok, picture = %Picture{}} <-
|
||||
Media.create_picture(%{"file" => args, "actor_id" => actor_id}) do
|
||||
{:ok,
|
||||
%{
|
||||
name: picture.file.name,
|
||||
url: picture.file.url,
|
||||
id: picture.id,
|
||||
content_type: picture.file.content_type,
|
||||
size: picture.file.size
|
||||
}}
|
||||
{:ok, media = %Media{}} <-
|
||||
Medias.create_media(%{"file" => args, "actor_id" => actor_id}) do
|
||||
{:ok, transform_media(media)}
|
||||
else
|
||||
{:error, :mime_type_not_allowed} ->
|
||||
{:error, dgettext("errors", "File doesn't have an allowed MIME type.")}
|
||||
@ -76,28 +66,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||
end
|
||||
end
|
||||
|
||||
def upload_picture(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
def upload_media(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@doc """
|
||||
Remove a picture that the user owns
|
||||
Remove a media that the user owns
|
||||
"""
|
||||
@spec remove_picture(map(), map(), map()) ::
|
||||
{:ok, Picture.t()}
|
||||
@spec remove_media(map(), map(), map()) ::
|
||||
{:ok, Media.t()}
|
||||
| {:error, :unauthorized}
|
||||
| {:error, :unauthenticated}
|
||||
| {:error, :not_found}
|
||||
def remove_picture(_parent, %{id: picture_id}, %{context: %{current_user: %User{} = user}}) do
|
||||
with {:picture, %Picture{actor_id: actor_id} = picture} <-
|
||||
{:picture, Media.get_picture(picture_id)},
|
||||
def remove_media(_parent, %{id: media_id}, %{context: %{current_user: %User{} = user}}) do
|
||||
with {:media, %Media{actor_id: actor_id} = media} <-
|
||||
{:media, Medias.get_media(media_id)},
|
||||
{:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id) do
|
||||
Media.delete_picture(picture)
|
||||
Medias.delete_media(media)
|
||||
else
|
||||
{:picture, nil} -> {:error, :not_found}
|
||||
{:media, nil} -> {:error, :not_found}
|
||||
{:is_owned, _} -> {:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
def remove_picture(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
def remove_media(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@doc """
|
||||
Return the total media size for an actor
|
||||
@ -108,7 +98,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||
context: %{current_user: %User{} = user}
|
||||
}) do
|
||||
if can_get_actor_size?(user, actor_id) do
|
||||
{:ok, Media.media_size_for_actor(actor_id)}
|
||||
{:ok, Medias.media_size_for_actor(actor_id)}
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
@ -125,7 +115,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||
context: %{current_user: %User{} = logged_user}
|
||||
}) do
|
||||
if can_get_user_size?(logged_user, user_id) do
|
||||
{:ok, Media.media_size_for_user(user_id)}
|
||||
{:ok, Medias.media_size_for_user(user_id)}
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
@ -133,6 +123,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||
|
||||
def user_size(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@spec transform_media(Media.t()) :: map()
|
||||
defp transform_media(%Media{id: id, file: file}) do
|
||||
%{
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
id: id,
|
||||
content_type: file.content_type,
|
||||
size: file.size
|
||||
}
|
||||
end
|
||||
|
||||
@spec can_get_user_size?(User.t(), integer()) :: boolean()
|
||||
defp can_get_actor_size?(%User{role: role} = user, actor_id) do
|
||||
role in [:moderator, :administrator] || owns_actor?(User.owns_actor(user, actor_id))
|
@ -13,6 +13,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
require Logger
|
||||
|
||||
alias Mobilizon.Web.{MediaProxy, Upload}
|
||||
|
||||
@ -137,6 +138,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
%{id: id} = args,
|
||||
%{context: %{current_user: user}} = _resolution
|
||||
) do
|
||||
require Logger
|
||||
args = Map.put(args, :user_id, user.id)
|
||||
|
||||
with {:find_actor, %Actor{} = actor} <-
|
||||
@ -198,11 +200,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
|
||||
defp save_attached_pictures(args) do
|
||||
Enum.reduce([:avatar, :banner], args, fn key, args ->
|
||||
if Map.has_key?(args, key) && !is_nil(args[key][:picture]) do
|
||||
pic = args[key][:picture]
|
||||
if Map.has_key?(args, key) && !is_nil(args[key][:media]) do
|
||||
media = args[key][:media]
|
||||
|
||||
with {:ok, %{name: name, url: url, content_type: content_type, size: _size}} <-
|
||||
Upload.store(pic.file, type: key, description: pic.alt) do
|
||||
Upload.store(media.file, type: key, description: media.alt) do
|
||||
Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type})
|
||||
end
|
||||
else
|
||||
|
@ -116,6 +116,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
||||
Map.update(args, :picture, nil, fn picture ->
|
||||
process_picture(picture, group)
|
||||
end),
|
||||
args <- extract_pictures_from_post_body(args, actor_id),
|
||||
{:ok, _, %Post{} = post} <-
|
||||
ActivityPub.create(
|
||||
:post,
|
||||
@ -156,6 +157,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
||||
Map.update(args, :picture, nil, fn picture ->
|
||||
process_picture(picture, group)
|
||||
end),
|
||||
args <- extract_pictures_from_post_body(args, actor_id),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Post{} = post} <-
|
||||
ActivityPub.update(post, args, true, %{"actor" => actor_url}) do
|
||||
@ -210,15 +212,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
||||
end
|
||||
|
||||
defp process_picture(nil, _), do: nil
|
||||
defp process_picture(%{picture_id: _picture_id} = args, _), do: args
|
||||
defp process_picture(%{media_id: _picture_id} = args, _), do: args
|
||||
|
||||
defp process_picture(%{picture: picture}, %Actor{id: actor_id}) do
|
||||
defp process_picture(%{media: media}, %Actor{id: actor_id}) do
|
||||
%{
|
||||
file:
|
||||
picture
|
||||
media
|
||||
|> Map.get(:file)
|
||||
|> Utils.make_picture_data(description: Map.get(picture, :name)),
|
||||
|> Utils.make_media_data(description: Map.get(media, :name)),
|
||||
actor_id: actor_id
|
||||
}
|
||||
end
|
||||
|
||||
@spec extract_pictures_from_post_body(map(), String.t()) :: map()
|
||||
defp extract_pictures_from_post_body(%{body: body} = args, actor_id) do
|
||||
pictures = Mobilizon.GraphQL.API.Utils.extract_pictures_from_body(body, actor_id)
|
||||
Map.put(args, :media, pictures)
|
||||
end
|
||||
|
||||
defp extract_pictures_from_post_body(args, _actor_id), do: args
|
||||
end
|
||||
|
@ -529,7 +529,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
context: %{current_user: %User{id: logged_in_user_id}}
|
||||
})
|
||||
when user_id == logged_in_user_id do
|
||||
%{elements: elements, total: total} = Mobilizon.Media.pictures_for_user(user_id, page, limit)
|
||||
%{elements: elements, total: total} = Mobilizon.Medias.medias_for_user(user_id, page, limit)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
|
@ -30,7 +30,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
import_types(Schema.Custom.Point)
|
||||
|
||||
import_types(Schema.UserType)
|
||||
import_types(Schema.PictureType)
|
||||
import_types(Schema.MediaType)
|
||||
import_types(Schema.ActorInterface)
|
||||
import_types(Schema.Actors.PersonType)
|
||||
import_types(Schema.Actors.GroupType)
|
||||
@ -145,7 +145,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
import_fields(:tag_queries)
|
||||
import_fields(:address_queries)
|
||||
import_fields(:config_queries)
|
||||
import_fields(:picture_queries)
|
||||
import_fields(:media_queries)
|
||||
import_fields(:report_queries)
|
||||
import_fields(:admin_queries)
|
||||
import_fields(:todo_list_queries)
|
||||
@ -168,7 +168,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
import_fields(:participant_mutations)
|
||||
import_fields(:member_mutations)
|
||||
import_fields(:feed_token_mutations)
|
||||
import_fields(:picture_mutations)
|
||||
import_fields(:media_mutations)
|
||||
import_fields(:report_mutations)
|
||||
import_fields(:admin_mutations)
|
||||
import_fields(:todo_list_mutations)
|
||||
|
@ -28,8 +28,8 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
|
||||
|
||||
field(:suspended, :boolean, description: "If the actor is suspended")
|
||||
|
||||
field(:avatar, :picture, description: "The actor's avatar picture")
|
||||
field(:banner, :picture, description: "The actor's banner picture")
|
||||
field(:avatar, :media, description: "The actor's avatar media")
|
||||
field(:banner, :media, description: "The actor's banner media")
|
||||
|
||||
# These one should have a privacy setting
|
||||
field(:following, list_of(:follower), description: "List of followings")
|
||||
|
@ -3,7 +3,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
|
||||
Schema representation for Group.
|
||||
"""
|
||||
|
||||
alias Mobilizon.GraphQL.Resolvers.Picture
|
||||
alias Mobilizon.GraphQL.Resolvers.Media
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
@desc """
|
||||
@ -27,8 +27,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
|
||||
|
||||
field(:suspended, :boolean, description: "If the actor is suspended")
|
||||
|
||||
field(:avatar, :picture, description: "The actor's avatar picture")
|
||||
field(:banner, :picture, description: "The actor's banner picture")
|
||||
field(:avatar, :media, description: "The actor's avatar media")
|
||||
field(:banner, :media, description: "The actor's banner media")
|
||||
|
||||
# These one should have a privacy setting
|
||||
field(:following, list_of(:follower), description: "List of followings")
|
||||
@ -37,7 +37,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
|
||||
field(:media_size, :integer,
|
||||
resolve: &Picture.actor_size/3,
|
||||
resolve: &Media.actor_size/3,
|
||||
description: "The total size of the media from this actor"
|
||||
)
|
||||
end
|
||||
|
@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.GraphQL.Resolvers.{Discussion, Group, Member, Picture, Post, Resource, Todos}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Discussion, Group, Media, Member, Post, Resource, Todos}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.Actors.MemberType)
|
||||
@ -38,8 +38,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
|
||||
field(:suspended, :boolean, description: "If the actor is suspended")
|
||||
|
||||
field(:avatar, :picture, description: "The actor's avatar picture")
|
||||
field(:banner, :picture, description: "The actor's banner picture")
|
||||
field(:avatar, :media, description: "The actor's avatar media")
|
||||
field(:banner, :media, description: "The actor's banner media")
|
||||
|
||||
field(:physical_address, :address,
|
||||
resolve: dataloader(Addresses),
|
||||
@ -53,7 +53,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
|
||||
field(:media_size, :integer,
|
||||
resolve: &Picture.actor_size/3,
|
||||
resolve: &Media.actor_size/3,
|
||||
description: "The total size of the media from this actor"
|
||||
)
|
||||
|
||||
@ -198,14 +198,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
default_value: :public
|
||||
)
|
||||
|
||||
arg(:avatar, :picture_input,
|
||||
arg(:avatar, :media_input,
|
||||
description:
|
||||
"The avatar for the group, either as an object or directly the ID of an existing Picture"
|
||||
"The avatar for the group, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
arg(:banner, :media_input,
|
||||
description:
|
||||
"The banner for the group, either as an object or directly the ID of an existing Picture"
|
||||
"The banner for the group, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:physical_address, :address_input, description: "The physical address for the group")
|
||||
@ -226,14 +226,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
description: "Whether the group can be join freely, with approval or is invite-only."
|
||||
)
|
||||
|
||||
arg(:avatar, :picture_input,
|
||||
arg(:avatar, :media_input,
|
||||
description:
|
||||
"The avatar for the group, either as an object or directly the ID of an existing Picture"
|
||||
"The avatar for the group, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
arg(:banner, :media_input,
|
||||
description:
|
||||
"The banner for the group, either as an object or directly the ID of an existing Picture"
|
||||
"The banner for the group, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:physical_address, :address_input, description: "The physical address for the group")
|
||||
|
@ -7,7 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.GraphQL.Resolvers.{Person, Picture}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Media, Person}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.Events.FeedTokenType)
|
||||
@ -40,8 +40,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
|
||||
field(:suspended, :boolean, description: "If the actor is suspended")
|
||||
|
||||
field(:avatar, :picture, description: "The actor's avatar picture")
|
||||
field(:banner, :picture, description: "The actor's banner picture")
|
||||
field(:avatar, :media, description: "The actor's avatar media")
|
||||
field(:banner, :media, description: "The actor's banner media")
|
||||
|
||||
# These one should have a privacy setting
|
||||
field(:following, list_of(:follower), description: "List of followings")
|
||||
@ -50,7 +50,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
|
||||
field(:media_size, :integer,
|
||||
resolve: &Picture.actor_size/3,
|
||||
resolve: &Media.actor_size/3,
|
||||
description: "The total size of the media from this actor"
|
||||
)
|
||||
|
||||
@ -150,14 +150,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
|
||||
arg(:summary, :string, description: "The summary for the new profile", default_value: "")
|
||||
|
||||
arg(:avatar, :picture_input,
|
||||
arg(:avatar, :media_input,
|
||||
description:
|
||||
"The avatar for the profile, either as an object or directly the ID of an existing Picture"
|
||||
"The avatar for the profile, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
arg(:banner, :media_input,
|
||||
description:
|
||||
"The banner for the profile, either as an object or directly the ID of an existing Picture"
|
||||
"The banner for the profile, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
resolve(&Person.create_person/3)
|
||||
@ -171,14 +171,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
|
||||
arg(:summary, :string, description: "The summary for this profile")
|
||||
|
||||
arg(:avatar, :picture_input,
|
||||
arg(:avatar, :media_input,
|
||||
description:
|
||||
"The avatar for the profile, either as an object or directly the ID of an existing Picture"
|
||||
"The avatar for the profile, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
arg(:banner, :media_input,
|
||||
description:
|
||||
"The banner for the profile, either as an object or directly the ID of an existing Picture"
|
||||
"The banner for the profile, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
resolve(&Person.update_person/3)
|
||||
@ -200,14 +200,14 @@ defmodule Mobilizon.GraphQL.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")
|
||||
|
||||
arg(:avatar, :picture_input,
|
||||
arg(:avatar, :media_input,
|
||||
description:
|
||||
"The avatar for the profile, either as an object or directly the ID of an existing Picture"
|
||||
"The avatar for the profile, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
arg(:banner, :media_input,
|
||||
description:
|
||||
"The banner for the profile, either as an object or directly the ID of an existing Picture"
|
||||
"The banner for the profile, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
resolve(&Person.register_person/3)
|
||||
|
@ -43,7 +43,6 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
|
||||
An address input
|
||||
"""
|
||||
input_object :address_input do
|
||||
# Either a full picture object
|
||||
field(:geom, :point, description: "The geocoordinates for the point where this address is")
|
||||
field(:street, :string, description: "The address's street name (with number)")
|
||||
field(:locality, :string, description: "The address's locality")
|
||||
|
@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.{Actors, Addresses, Discussions}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Event, Picture, Tag}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Event, Media, Tag}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.AddressType)
|
||||
@ -31,9 +31,14 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
field(:visibility, :event_visibility, description: "The event's visibility")
|
||||
field(:join_options, :event_join_options, description: "The event's visibility")
|
||||
|
||||
field(:picture, :picture,
|
||||
field(:picture, :media,
|
||||
description: "The event's picture",
|
||||
resolve: &Picture.picture/3
|
||||
resolve: &Media.media/3
|
||||
)
|
||||
|
||||
field(:media, list_of(:media),
|
||||
description: "The event's media",
|
||||
resolve: &Media.medias/3
|
||||
)
|
||||
|
||||
field(:publish_at, :datetime, description: "When the event was published")
|
||||
@ -328,9 +333,9 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
description: "The list of tags associated to the event"
|
||||
)
|
||||
|
||||
arg(:picture, :picture_input,
|
||||
arg(:picture, :media_input,
|
||||
description:
|
||||
"The picture for the event, either as an object or directly the ID of an existing Picture"
|
||||
"The picture for the event, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:publish_at, :datetime, description: "Datetime when the event was published")
|
||||
@ -379,9 +384,9 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
|
||||
arg(:tags, list_of(:string), description: "The list of tags associated to the event")
|
||||
|
||||
arg(:picture, :picture_input,
|
||||
arg(:picture, :media_input,
|
||||
description:
|
||||
"The picture for the event, either as an object or directly the ID of an existing Picture"
|
||||
"The picture for the event, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
arg(:online_address, :string, description: "Online address of the event")
|
||||
|
68
lib/graphql/schema/media.ex
Normal file
68
lib/graphql/schema/media.ex
Normal file
@ -0,0 +1,68 @@
|
||||
defmodule Mobilizon.GraphQL.Schema.MediaType do
|
||||
@moduledoc """
|
||||
Schema representation for Medias
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
alias Mobilizon.GraphQL.Resolvers.Media
|
||||
|
||||
@desc "A media"
|
||||
object :media do
|
||||
field(:id, :id, description: "The media's ID")
|
||||
field(:alt, :string, description: "The media's alternative text")
|
||||
field(:name, :string, description: "The media's name")
|
||||
field(:url, :string, description: "The media's full URL")
|
||||
field(:content_type, :string, description: "The media's detected content type")
|
||||
field(:size, :integer, description: "The media's size")
|
||||
end
|
||||
|
||||
@desc """
|
||||
A paginated list of medias
|
||||
"""
|
||||
object :paginated_media_list do
|
||||
field(:elements, list_of(:media), description: "The list of medias")
|
||||
field(:total, :integer, description: "The total number of medias in the list")
|
||||
end
|
||||
|
||||
@desc "An attached media or a link to a media"
|
||||
input_object :media_input do
|
||||
# Either a full media object
|
||||
field(:media, :media_input_object, description: "A full media attached")
|
||||
# Or directly the ID of an existing media
|
||||
field(:media_id, :id, description: "The ID of an existing media")
|
||||
end
|
||||
|
||||
@desc "An attached media"
|
||||
input_object :media_input_object do
|
||||
field(:name, non_null(:string), description: "The media's name")
|
||||
field(:alt, :string, description: "The media's alternative text")
|
||||
field(:file, non_null(:upload), description: "The media file")
|
||||
field(:actor_id, :id, description: "The media owner")
|
||||
end
|
||||
|
||||
object :media_queries do
|
||||
@desc "Get a media"
|
||||
field :media, :media do
|
||||
arg(:id, non_null(:id), description: "The media ID")
|
||||
resolve(&Media.media/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :media_mutations do
|
||||
@desc "Upload a media"
|
||||
field :upload_media, :media do
|
||||
arg(:name, non_null(:string), description: "The media's name")
|
||||
arg(:alt, :string, description: "The media's alternative text")
|
||||
arg(:file, non_null(:upload), description: "The media file")
|
||||
resolve(&Media.upload_media/3)
|
||||
end
|
||||
|
||||
@desc """
|
||||
Remove a media
|
||||
"""
|
||||
field :remove_media, :deleted_object do
|
||||
arg(:id, non_null(:id), description: "The media's ID")
|
||||
resolve(&Media.remove_media/3)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,68 +0,0 @@
|
||||
defmodule Mobilizon.GraphQL.Schema.PictureType do
|
||||
@moduledoc """
|
||||
Schema representation for Pictures
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
alias Mobilizon.GraphQL.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")
|
||||
field(:content_type, :string, description: "The picture's detected content type")
|
||||
field(:size, :integer, description: "The picture's size")
|
||||
end
|
||||
|
||||
@desc """
|
||||
A paginated list of pictures
|
||||
"""
|
||||
object :paginated_picture_list do
|
||||
field(:elements, list_of(:picture), description: "The list of pictures")
|
||||
field(:total, :integer, description: "The total number of pictures in the list")
|
||||
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, description: "A full picture attached")
|
||||
# Or directly the ID of an existing picture
|
||||
field(:picture_id, :id, description: "The ID of an existing picture")
|
||||
end
|
||||
|
||||
@desc "An attached picture"
|
||||
input_object :picture_input_object do
|
||||
field(:name, non_null(:string), description: "The picture's name")
|
||||
field(:alt, :string, description: "The picture's alternative text")
|
||||
field(:file, non_null(:upload), description: "The picture file")
|
||||
field(:actor_id, :id, description: "The picture owner")
|
||||
end
|
||||
|
||||
object :picture_queries do
|
||||
@desc "Get a picture"
|
||||
field :picture, :picture do
|
||||
arg(:id, non_null(:id), description: "The picture ID")
|
||||
resolve(&Picture.picture/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :picture_mutations do
|
||||
@desc "Upload a picture"
|
||||
field :upload_picture, :picture do
|
||||
arg(:name, non_null(:string), description: "The picture's name")
|
||||
arg(:alt, :string, description: "The picture's alternative text")
|
||||
arg(:file, non_null(:upload), description: "The picture file")
|
||||
resolve(&Picture.upload_picture/3)
|
||||
end
|
||||
|
||||
@desc """
|
||||
Remove a picture
|
||||
"""
|
||||
field :remove_picture, :deleted_object do
|
||||
arg(:id, non_null(:id), description: "The picture's ID")
|
||||
resolve(&Picture.remove_picture/3)
|
||||
end
|
||||
end
|
||||
end
|
@ -3,7 +3,7 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
|
||||
Schema representation for Posts
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
alias Mobilizon.GraphQL.Resolvers.{Picture, Post, Tag}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Media, Post, Tag}
|
||||
|
||||
@desc "A post"
|
||||
object :post do
|
||||
@ -25,9 +25,9 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
|
||||
description: "The post's tags"
|
||||
)
|
||||
|
||||
field(:picture, :picture,
|
||||
description: "The posts's picture",
|
||||
resolve: &Picture.picture/3
|
||||
field(:picture, :media,
|
||||
description: "The posts's media",
|
||||
resolve: &Media.media/3
|
||||
)
|
||||
end
|
||||
|
||||
@ -76,9 +76,9 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
|
||||
description: "The list of tags associated to the post"
|
||||
)
|
||||
|
||||
arg(:picture, :picture_input,
|
||||
arg(:picture, :media_input,
|
||||
description:
|
||||
"The banner for the post, either as an object or directly the ID of an existing Picture"
|
||||
"The banner for the post, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
resolve(&Post.create_post/3)
|
||||
@ -99,9 +99,9 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
|
||||
|
||||
arg(:tags, list_of(:string), description: "The list of tags associated to the post")
|
||||
|
||||
arg(:picture, :picture_input,
|
||||
arg(:picture, :media_input,
|
||||
description:
|
||||
"The banner for the post, either as an object or directly the ID of an existing Picture"
|
||||
"The banner for the post, either as an object or directly the ID of an existing media"
|
||||
)
|
||||
|
||||
resolve(&Post.update_post/3)
|
||||
|
@ -7,7 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.GraphQL.Resolvers.{Picture, User}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Media, User}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.SortType)
|
||||
@ -111,7 +111,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
description: "The IP adress the user's currently signed-in with"
|
||||
)
|
||||
|
||||
field(:media, :paginated_picture_list, description: "The user's media objects") do
|
||||
field(:media, :paginated_media_list, description: "The user's media objects") do
|
||||
arg(:page, :integer,
|
||||
default_value: 1,
|
||||
description: "The page in the paginated user media list"
|
||||
@ -122,7 +122,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
end
|
||||
|
||||
field(:media_size, :integer,
|
||||
resolve: &Picture.user_size/3,
|
||||
resolve: &Media.user_size/3,
|
||||
description: "The total size of all the media from this user (from all their actors)"
|
||||
)
|
||||
end
|
||||
|
@ -47,12 +47,14 @@ defmodule Mix.Tasks.Mobilizon.Common do
|
||||
else: shell_prompt(message, "Continue?") in ~w(Yn Y y)
|
||||
end
|
||||
|
||||
@spec shell_info(String.t()) :: :ok
|
||||
def shell_info(message) do
|
||||
if mix_shell?(),
|
||||
do: Mix.shell().info(message),
|
||||
else: IO.puts(message)
|
||||
end
|
||||
|
||||
@spec shell_error(String.t()) :: :ok
|
||||
def shell_error(message) do
|
||||
if mix_shell?(),
|
||||
do: Mix.shell().error(message),
|
||||
|
23
lib/mix/tasks/mobilizon/maintenance.ex
Normal file
23
lib/mix/tasks/mobilizon/maintenance.ex
Normal file
@ -0,0 +1,23 @@
|
||||
defmodule Mix.Tasks.Mobilizon.Maintenance do
|
||||
@moduledoc """
|
||||
Tasks to maintain mobilizon
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
|
||||
alias Mix.Tasks
|
||||
import Mix.Tasks.Mobilizon.Common
|
||||
|
||||
@shortdoc "List common Mobilizon maintenance tasks"
|
||||
|
||||
@impl Mix.Task
|
||||
def run(_) do
|
||||
shell_info("\nAvailable tasks:")
|
||||
|
||||
if mix_shell?() do
|
||||
Tasks.Help.run(["--search", "mobilizon.maintenance."])
|
||||
else
|
||||
show_subtasks_for_module(__MODULE__)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,107 @@
|
||||
defmodule Mix.Tasks.Mobilizon.Maintenance.FixUnattachedMediaInBody do
|
||||
@moduledoc """
|
||||
Task to reattach media files that were added in event, post or comment bodies without being attached to their entities.
|
||||
|
||||
This task should only be run once.
|
||||
"""
|
||||
use Mix.Task
|
||||
|
||||
import Mix.Tasks.Mobilizon.Common
|
||||
alias Mobilizon.{Discussions, Events, Medias, Posts}
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Storage.Repo
|
||||
require Logger
|
||||
|
||||
@preferred_cli_env "prod"
|
||||
|
||||
# TODO: Remove me in Mobilizon 1.2
|
||||
|
||||
@shortdoc "Reattaches inline media from events and posts"
|
||||
def run([]) do
|
||||
start_mobilizon()
|
||||
|
||||
shell_info("Going to extract pictures from events")
|
||||
extract_inline_pictures_from_bodies(Event)
|
||||
shell_info("Going to extract pictures from posts")
|
||||
extract_inline_pictures_from_bodies(Post)
|
||||
shell_info("Going to extract pictures from comments")
|
||||
extract_inline_pictures_from_bodies(Comment)
|
||||
end
|
||||
|
||||
defp extract_inline_pictures_from_bodies(entity) do
|
||||
Repo.transaction(
|
||||
fn ->
|
||||
entity
|
||||
|> Repo.stream()
|
||||
|> Stream.map(&extract_pictures(&1))
|
||||
|> Stream.map(fn {entity, pics} -> save_entity(entity, pics) end)
|
||||
|> Stream.run()
|
||||
end,
|
||||
timeout: :infinity
|
||||
)
|
||||
end
|
||||
|
||||
defp extract_pictures(entity) do
|
||||
extracted_pictures = entity |> get_body() |> parse_body() |> get_media_entities_from_urls()
|
||||
|
||||
attached_picture = entity |> get_picture() |> get_media_entity_from_media_id()
|
||||
attached_pictures = [attached_picture] |> Enum.filter(& &1)
|
||||
|
||||
{entity, extracted_pictures ++ attached_pictures}
|
||||
end
|
||||
|
||||
defp get_body(%Event{description: description}), do: description
|
||||
defp get_body(%Post{body: body}), do: body
|
||||
defp get_body(%Comment{text: text}), do: text
|
||||
|
||||
defp get_picture(%Event{picture_id: picture_id}), do: picture_id
|
||||
defp get_picture(%Post{picture_id: picture_id}), do: picture_id
|
||||
defp get_picture(%Comment{}), do: nil
|
||||
|
||||
defp parse_body(nil), do: []
|
||||
|
||||
defp parse_body(body) do
|
||||
with res <- Regex.scan(~r/<img\s[^>]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/, body),
|
||||
res <- Enum.map(res, fn [_, res] -> res end) do
|
||||
res
|
||||
end
|
||||
end
|
||||
|
||||
defp get_media_entities_from_urls(media_urls) do
|
||||
media_urls
|
||||
|> Enum.map(fn media_url ->
|
||||
# We prefer orphan media, but fallback on already attached media just in case
|
||||
Medias.get_unattached_media_by_url(media_url) || Medias.get_media_by_url(media_url)
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
||||
defp get_media_entity_from_media_id(nil), do: nil
|
||||
|
||||
defp get_media_entity_from_media_id(media_id) do
|
||||
Medias.get_media(media_id)
|
||||
end
|
||||
|
||||
defp save_entity(%Event{} = _event, []), do: :ok
|
||||
|
||||
defp save_entity(%Event{} = event, media) do
|
||||
event = Repo.preload(event, [:contacts, :media])
|
||||
Events.update_event(event, %{media: media})
|
||||
end
|
||||
|
||||
defp save_entity(%Post{} = _post, []), do: :ok
|
||||
|
||||
defp save_entity(%Post{} = post, media) do
|
||||
post = Repo.preload(post, [:media])
|
||||
Posts.update_post(post, %{media: media})
|
||||
end
|
||||
|
||||
defp save_entity(%Comment{} = _comment, []), do: :ok
|
||||
|
||||
defp save_entity(%Comment{} = comment, media) do
|
||||
comment = Repo.preload(comment, [:media])
|
||||
Discussions.update_comment(comment, %{media: media})
|
||||
end
|
||||
end
|
23
lib/mix/tasks/mobilizon/media.ex
Normal file
23
lib/mix/tasks/mobilizon/media.ex
Normal file
@ -0,0 +1,23 @@
|
||||
defmodule Mix.Tasks.Mobilizon.Media do
|
||||
@moduledoc """
|
||||
Tasks to manage media
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
|
||||
alias Mix.Tasks
|
||||
import Mix.Tasks.Mobilizon.Common
|
||||
|
||||
@shortdoc "Manages Mobilizon media"
|
||||
|
||||
@impl Mix.Task
|
||||
def run(_) do
|
||||
shell_info("\nAvailable tasks:")
|
||||
|
||||
if mix_shell?() do
|
||||
Tasks.Help.run(["--search", "mobilizon.media."])
|
||||
else
|
||||
show_subtasks_for_module(__MODULE__)
|
||||
end
|
||||
end
|
||||
end
|
87
lib/mix/tasks/mobilizon/media/clean_orphan.ex
Normal file
87
lib/mix/tasks/mobilizon/media/clean_orphan.ex
Normal file
@ -0,0 +1,87 @@
|
||||
defmodule Mix.Tasks.Mobilizon.Media.CleanOrphan do
|
||||
@moduledoc """
|
||||
Task to accept an instance follow request
|
||||
"""
|
||||
use Mix.Task
|
||||
import Mix.Tasks.Mobilizon.Common
|
||||
alias Mobilizon.Service.CleanOrphanMedia
|
||||
|
||||
@shortdoc "Clean orphan media"
|
||||
|
||||
@grace_period Mobilizon.Config.get([:instance, :orphan_upload_grace_period_hours], 48)
|
||||
|
||||
@impl Mix.Task
|
||||
def run(options) do
|
||||
{options, [], []} =
|
||||
OptionParser.parse(
|
||||
options,
|
||||
strict: [
|
||||
dry_run: :boolean,
|
||||
days: :integer,
|
||||
verbose: :boolean
|
||||
],
|
||||
aliases: [
|
||||
d: :days,
|
||||
v: :verbose
|
||||
]
|
||||
)
|
||||
|
||||
dry_run = Keyword.get(options, :dry_run, false)
|
||||
grace_period = Keyword.get(options, :days)
|
||||
grace_period = if is_nil(grace_period), do: @grace_period, else: grace_period * 24
|
||||
verbose = Keyword.get(options, :verbose, false)
|
||||
|
||||
start_mobilizon()
|
||||
|
||||
case CleanOrphanMedia.clean(dry_run: dry_run, grace_period: grace_period) do
|
||||
{:ok, medias} ->
|
||||
if length(medias) > 0 do
|
||||
if dry_run or verbose do
|
||||
details(medias, dry_run, verbose)
|
||||
end
|
||||
|
||||
result(dry_run, length(medias))
|
||||
else
|
||||
empty_result(dry_run)
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
_err ->
|
||||
shell_error("Error while cleaning orphan media files")
|
||||
end
|
||||
end
|
||||
|
||||
@spec details(list(Media.t()), boolean(), boolean()) :: :ok
|
||||
defp details(medias, dry_run, verbose) do
|
||||
cond do
|
||||
dry_run ->
|
||||
shell_info("List of files that would have been deleted")
|
||||
|
||||
verbose ->
|
||||
shell_info("List of files that have been deleted")
|
||||
end
|
||||
|
||||
Enum.each(medias, fn media ->
|
||||
shell_info("ID: #{media.id}, Actor: #{media.actor_id}, URL: #{media.file.url}")
|
||||
end)
|
||||
end
|
||||
|
||||
@spec result(boolean(), boolean()) :: :ok
|
||||
defp result(dry_run, nb_medias) do
|
||||
if dry_run do
|
||||
shell_info("#{nb_medias} files would have been deleted")
|
||||
else
|
||||
shell_info("#{nb_medias} files have been deleted")
|
||||
end
|
||||
end
|
||||
|
||||
@spec empty_result(boolean()) :: :ok
|
||||
defp empty_result(dry_run) do
|
||||
if dry_run do
|
||||
shell_info("No files would have been deleted")
|
||||
else
|
||||
shell_info("No files were deleted")
|
||||
end
|
||||
end
|
||||
end
|
@ -58,7 +58,11 @@ defmodule Mobilizon do
|
||||
cachex_spec(:statistics, 10, 60, 60),
|
||||
cachex_spec(:config, 10, 60, 60),
|
||||
cachex_spec(:rich_media_cache, 10, 60, 60),
|
||||
cachex_spec(:activity_pub, 2500, 3, 15)
|
||||
cachex_spec(:activity_pub, 2500, 3, 15),
|
||||
%{
|
||||
id: :cache_key_value,
|
||||
start: {Cachex, :start_link, [:key_value]}
|
||||
}
|
||||
] ++
|
||||
task_children(@env)
|
||||
|
||||
|
@ -12,7 +12,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Media.File
|
||||
alias Mobilizon.Medias.File
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
|
@ -14,7 +14,7 @@ defmodule Mobilizon.Actors do
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.{Crypto, Events}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Media.File
|
||||
alias Mobilizon.Medias.File
|
||||
alias Mobilizon.Service.Workers
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users
|
||||
@ -285,7 +285,7 @@ defmodule Mobilizon.Actors do
|
||||
# if is_nil(file) do
|
||||
# nil
|
||||
# else
|
||||
# struct(Mobilizon.Media.File, file)
|
||||
# struct(Mobilizon.Medias.File, file)
|
||||
# end
|
||||
# end
|
||||
|
||||
@ -1673,7 +1673,8 @@ defmodule Mobilizon.Actors do
|
||||
:attributed_to,
|
||||
:tags,
|
||||
:physical_address,
|
||||
:contacts
|
||||
:contacts,
|
||||
:media
|
||||
])
|
||||
|
||||
ActivityPub.delete(event, actor, false)
|
||||
|
@ -11,6 +11,7 @@ defmodule Mobilizon.Discussions.Comment do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion}
|
||||
alias Mobilizon.Events.{Event, Tag}
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Mention
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
@ -27,6 +28,7 @@ defmodule Mobilizon.Discussions.Comment do
|
||||
event: Event.t(),
|
||||
tags: [Tag.t()],
|
||||
mentions: [Mention.t()],
|
||||
media: [Media.t()],
|
||||
in_reply_to_comment: t,
|
||||
origin_comment: t
|
||||
}
|
||||
@ -66,6 +68,7 @@ defmodule Mobilizon.Discussions.Comment do
|
||||
has_many(:replies, Comment, foreign_key: :in_reply_to_comment_id)
|
||||
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
|
||||
has_many(:mentions, Mention)
|
||||
many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
@ -120,6 +123,7 @@ defmodule Mobilizon.Discussions.Comment do
|
||||
|> maybe_add_published_at()
|
||||
|> maybe_generate_uuid()
|
||||
|> maybe_generate_url()
|
||||
|> put_assoc(:media, Map.get(attrs, :media, []))
|
||||
|> put_tags(attrs)
|
||||
|> put_mentions(attrs)
|
||||
end
|
||||
|
@ -10,7 +10,7 @@ defmodule Mobilizon.Events.Event do
|
||||
alias Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.{Addresses, Events, Media, Mention}
|
||||
alias Mobilizon.{Addresses, Events, Medias, Mention}
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
alias Mobilizon.Discussions.Comment
|
||||
@ -27,7 +27,7 @@ defmodule Mobilizon.Events.Event do
|
||||
Track
|
||||
}
|
||||
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
@ -54,7 +54,8 @@ defmodule Mobilizon.Events.Event do
|
||||
organizer_actor: Actor.t(),
|
||||
attributed_to: Actor.t(),
|
||||
physical_address: Address.t(),
|
||||
picture: Picture.t(),
|
||||
picture: Media.t(),
|
||||
media: [Media.t()],
|
||||
tracks: [Track.t()],
|
||||
sessions: [Session.t()],
|
||||
mentions: [Mention.t()],
|
||||
@ -110,7 +111,7 @@ defmodule Mobilizon.Events.Event do
|
||||
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
|
||||
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
|
||||
belongs_to(:physical_address, Address, on_replace: :nilify)
|
||||
belongs_to(:picture, Picture, on_replace: :update)
|
||||
belongs_to(:picture, Media, on_replace: :update)
|
||||
has_many(:tracks, Track)
|
||||
has_many(:sessions, Session)
|
||||
has_many(:mentions, Mention)
|
||||
@ -118,6 +119,7 @@ defmodule Mobilizon.Events.Event do
|
||||
many_to_many(:contacts, Actor, join_through: "event_contacts", on_replace: :delete)
|
||||
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
|
||||
many_to_many(:participants, Actor, join_through: Participant)
|
||||
many_to_many(:media, Media, join_through: "events_medias", on_replace: :delete)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
@ -150,6 +152,7 @@ defmodule Mobilizon.Events.Event do
|
||||
changeset
|
||||
|> cast_embed(:options)
|
||||
|> put_assoc(:contacts, Map.get(attrs, :contacts, []))
|
||||
|> put_assoc(:media, Map.get(attrs, :media, []))
|
||||
|> put_tags(attrs)
|
||||
|> put_address(attrs)
|
||||
|> put_picture(attrs)
|
||||
@ -241,9 +244,9 @@ defmodule Mobilizon.Events.Event do
|
||||
|
||||
# In case the provided picture is an existing one
|
||||
@spec put_picture(Changeset.t(), map) :: Changeset.t()
|
||||
defp put_picture(%Changeset{} = changeset, %{picture: %{picture_id: id} = _picture}) do
|
||||
case Media.get_picture!(id) do
|
||||
%Picture{} = picture ->
|
||||
defp put_picture(%Changeset{} = changeset, %{picture: %{media_id: id} = _picture}) do
|
||||
case Medias.get_media!(id) do
|
||||
%Media{} = picture ->
|
||||
put_assoc(changeset, :picture, picture)
|
||||
|
||||
_ ->
|
||||
|
@ -84,7 +84,8 @@ defmodule Mobilizon.Events do
|
||||
:participants,
|
||||
:physical_address,
|
||||
:picture,
|
||||
:contacts
|
||||
:contacts,
|
||||
:media
|
||||
]
|
||||
|
||||
@doc """
|
||||
@ -295,7 +296,7 @@ defmodule Mobilizon.Events do
|
||||
@spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Changeset.t()}
|
||||
def update_event(%Event{draft: old_draft} = old_event, attrs) do
|
||||
with %Changeset{changes: changes} = changeset <-
|
||||
Event.update_changeset(Repo.preload(old_event, :tags), attrs),
|
||||
Event.update_changeset(Repo.preload(old_event, [:tags, :media]), attrs),
|
||||
{:ok, %{update: %Event{} = new_event}} <-
|
||||
Multi.new()
|
||||
|> Multi.update(:update, changeset)
|
||||
|
@ -1,150 +0,0 @@
|
||||
defmodule Mobilizon.Media do
|
||||
@moduledoc """
|
||||
The Media context.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Ecto.Multi
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.{File, Picture}
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.Web.Upload
|
||||
|
||||
@doc """
|
||||
Gets a single picture.
|
||||
"""
|
||||
@spec get_picture(integer | String.t()) :: Picture.t() | nil
|
||||
def get_picture(id), do: Repo.get(Picture, id)
|
||||
|
||||
@doc """
|
||||
Gets a single picture.
|
||||
Raises `Ecto.NoResultsError` if the picture does not exist.
|
||||
"""
|
||||
@spec get_picture!(integer | String.t()) :: Picture.t()
|
||||
def get_picture!(id), do: Repo.get!(Picture, id)
|
||||
|
||||
@doc """
|
||||
Get a picture by its URL.
|
||||
"""
|
||||
@spec get_picture_by_url(String.t()) :: Picture.t() | nil
|
||||
def get_picture_by_url(url) do
|
||||
url
|
||||
|> picture_by_url_query()
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
List the paginated picture for an actor
|
||||
"""
|
||||
@spec pictures_for_actor(integer | String.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def pictures_for_actor(actor_id, page, limit) do
|
||||
actor_id
|
||||
|> pictures_for_actor_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
List the paginated picture for user
|
||||
"""
|
||||
@spec pictures_for_user(integer | String.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def pictures_for_user(user_id, page, limit) do
|
||||
user_id
|
||||
|> pictures_for_user_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculate the sum of media size used by the user
|
||||
"""
|
||||
@spec media_size_for_actor(integer | String.t()) :: integer()
|
||||
def media_size_for_actor(actor_id) do
|
||||
actor_id
|
||||
|> pictures_for_actor_query()
|
||||
|> select([:file])
|
||||
|> Repo.all()
|
||||
|> Enum.map(& &1.file.size)
|
||||
|> Enum.sum()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculate the sum of media size used by the user
|
||||
"""
|
||||
@spec media_size_for_user(integer | String.t()) :: integer()
|
||||
def media_size_for_user(user_id) do
|
||||
user_id
|
||||
|> pictures_for_user_query()
|
||||
|> select([:file])
|
||||
|> Repo.all()
|
||||
|> Enum.map(& &1.file.size)
|
||||
|> Enum.sum()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a picture.
|
||||
"""
|
||||
@spec create_picture(map) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_picture(attrs \\ %{}) do
|
||||
%Picture{}
|
||||
|> Picture.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a picture.
|
||||
"""
|
||||
@spec update_picture(Picture.t(), map) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_picture(%Picture{} = picture, attrs) do
|
||||
picture
|
||||
|> Picture.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a picture.
|
||||
"""
|
||||
@spec delete_picture(Picture.t()) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_picture(%Picture{} = picture) do
|
||||
transaction =
|
||||
Multi.new()
|
||||
|> Multi.delete(:picture, picture)
|
||||
|> Multi.run(:remove, fn _repo, %{picture: %Picture{file: %File{url: url}}} ->
|
||||
Upload.remove(url)
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
|
||||
case transaction do
|
||||
{:ok, %{picture: %Picture{} = picture}} ->
|
||||
{:ok, picture}
|
||||
|
||||
{:error, :remove, error, _} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec picture_by_url_query(String.t()) :: Ecto.Query.t()
|
||||
defp picture_by_url_query(url) do
|
||||
from(
|
||||
p in Picture,
|
||||
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
|
||||
)
|
||||
end
|
||||
|
||||
@spec pictures_for_actor_query(integer() | String.t()) :: Ecto.Query.t()
|
||||
defp pictures_for_actor_query(actor_id) do
|
||||
Picture
|
||||
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|
||||
|> where([_p, a], a.id == ^actor_id)
|
||||
end
|
||||
|
||||
@spec pictures_for_user_query(integer() | String.t()) :: Ecto.Query.t()
|
||||
defp pictures_for_user_query(user_id) do
|
||||
Picture
|
||||
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|
||||
|> join(:inner, [_p, a], u in User, on: a.user_id == u.id)
|
||||
|> where([_p, _a, u], u.id == ^user_id)
|
||||
end
|
||||
end
|
@ -1,32 +0,0 @@
|
||||
defmodule Mobilizon.Media.Picture do
|
||||
@moduledoc """
|
||||
Represents a picture entity.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset, only: [cast: 3, cast_embed: 2]
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.File
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
file: File.t(),
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
schema "pictures" do
|
||||
embeds_one(:file, File, on_replace: :update)
|
||||
belongs_to(:actor, Actor)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = picture, attrs) do
|
||||
picture
|
||||
|> cast(attrs, [:actor_id])
|
||||
|> cast_embed(:file)
|
||||
end
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
defmodule Mobilizon.Media.File do
|
||||
defmodule Mobilizon.Medias.File do
|
||||
@moduledoc """
|
||||
Represents a file entity.
|
||||
"""
|
40
lib/mobilizon/medias/media.ex
Normal file
40
lib/mobilizon/medias/media.ex
Normal file
@ -0,0 +1,40 @@
|
||||
defmodule Mobilizon.Medias.Media do
|
||||
@moduledoc """
|
||||
Represents a media entity.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset, only: [cast: 3, cast_embed: 2]
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Medias.File
|
||||
alias Mobilizon.Posts.Post
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
file: File.t(),
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
schema "medias" do
|
||||
embeds_one(:file, File, on_replace: :update)
|
||||
belongs_to(:actor, Actor)
|
||||
has_many(:event_picture, Event, foreign_key: :picture_id)
|
||||
many_to_many(:events, Event, join_through: "events_medias")
|
||||
has_many(:posts_picture, Post, foreign_key: :picture_id)
|
||||
many_to_many(:posts, Post, join_through: "posts_medias")
|
||||
many_to_many(:comments, Comment, join_through: "comments_medias")
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = media, attrs) do
|
||||
media
|
||||
|> cast(attrs, [:actor_id])
|
||||
|> cast_embed(:file)
|
||||
end
|
||||
end
|
184
lib/mobilizon/medias/medias.ex
Normal file
184
lib/mobilizon/medias/medias.ex
Normal file
@ -0,0 +1,184 @@
|
||||
defmodule Mobilizon.Medias do
|
||||
@moduledoc """
|
||||
The Media context.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Ecto.Multi
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Medias.{File, Media}
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.Web.Upload
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Gets a single media.
|
||||
"""
|
||||
@spec get_media(integer | String.t()) :: Media.t() | nil
|
||||
def get_media(id), do: Repo.get(Media, id)
|
||||
|
||||
@doc """
|
||||
Gets a single media.
|
||||
Raises `Ecto.NoResultsError` if the media does not exist.
|
||||
"""
|
||||
@spec get_media!(integer | String.t()) :: Media.t()
|
||||
def get_media!(id), do: Repo.get!(Media, id)
|
||||
|
||||
@doc """
|
||||
Get a media by its URL.
|
||||
"""
|
||||
@spec get_media_by_url(String.t()) :: Media.t() | nil
|
||||
def get_media_by_url(url) do
|
||||
url
|
||||
|> media_by_url_query()
|
||||
|> limit(1)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get an unattached media by it's URL
|
||||
"""
|
||||
def get_unattached_media_by_url(url) do
|
||||
url
|
||||
|> media_by_url_query()
|
||||
|> join(:left, [m], e in assoc(m, :events))
|
||||
|> join(:left, [m], ep in assoc(m, :event_picture))
|
||||
|> join(:left, [m], p in assoc(m, :posts))
|
||||
|> join(:left, [m], pp in assoc(m, :posts_picture))
|
||||
|> join(:left, [m], c in assoc(m, :comments))
|
||||
|> where([_m, e], is_nil(e.id))
|
||||
|> where([_m, _e, ep], is_nil(ep.id))
|
||||
|> where([_m, _e, _ep, p], is_nil(p.id))
|
||||
|> where([_m, _e, _ep, _p, pp], is_nil(pp.id))
|
||||
|> where([_m, _e, _ep, _p, _pp, c], is_nil(c.id))
|
||||
|> limit(1)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
List the paginated media for an actor
|
||||
"""
|
||||
@spec medias_for_actor(integer | String.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def medias_for_actor(actor_id, page, limit) do
|
||||
actor_id
|
||||
|> medias_for_actor_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
List the paginated media for user
|
||||
"""
|
||||
@spec medias_for_user(integer | String.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def medias_for_user(user_id, page, limit) do
|
||||
user_id
|
||||
|> medias_for_user_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculate the sum of media size used by the user
|
||||
"""
|
||||
@spec media_size_for_actor(integer | String.t()) :: integer()
|
||||
def media_size_for_actor(actor_id) do
|
||||
actor_id
|
||||
|> medias_for_actor_query()
|
||||
|> select([:file])
|
||||
|> Repo.all()
|
||||
|> Enum.map(& &1.file.size)
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.sum()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculate the sum of media size used by the user
|
||||
"""
|
||||
@spec media_size_for_user(integer | String.t()) :: integer()
|
||||
def media_size_for_user(user_id) do
|
||||
user_id
|
||||
|> medias_for_user_query()
|
||||
|> select([:file])
|
||||
|> Repo.all()
|
||||
|> Enum.map(& &1.file.size)
|
||||
|> Enum.sum()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a media.
|
||||
"""
|
||||
@spec create_media(map) :: {:ok, Media.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_media(attrs \\ %{}) do
|
||||
%Media{}
|
||||
|> Media.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a media.
|
||||
"""
|
||||
@spec update_media(Media.t(), map) :: {:ok, Media.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_media(%Media{} = media, attrs) do
|
||||
media
|
||||
|> Media.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a media.
|
||||
"""
|
||||
@spec delete_media(Media.t()) :: {:ok, Media.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_media(%Media{} = media, opts \\ []) do
|
||||
transaction =
|
||||
Multi.new()
|
||||
|> Multi.delete(:media, media)
|
||||
|> Multi.run(:remove, fn _repo, %{media: %Media{file: %File{url: url}} = media} ->
|
||||
case Upload.remove(url) do
|
||||
{:error, err} ->
|
||||
if err =~ "doesn't exist" and Keyword.get(opts, :ignore_file_not_found, false) do
|
||||
Logger.info("Deleting media and ignoring absent file.")
|
||||
{:ok, media}
|
||||
else
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:ok, media} ->
|
||||
{:ok, media}
|
||||
end
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
|
||||
case transaction do
|
||||
{:ok, %{media: %Media{} = media}} ->
|
||||
{:ok, media}
|
||||
|
||||
{:error, :remove, error, _} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec media_by_url_query(String.t()) :: Ecto.Query.t()
|
||||
defp media_by_url_query(url) do
|
||||
from(
|
||||
p in Media,
|
||||
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
|
||||
)
|
||||
end
|
||||
|
||||
@spec medias_for_actor_query(integer() | String.t()) :: Ecto.Query.t()
|
||||
defp medias_for_actor_query(actor_id) do
|
||||
Media
|
||||
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|
||||
|> where([_p, a], a.id == ^actor_id)
|
||||
end
|
||||
|
||||
@spec medias_for_user_query(integer() | String.t()) :: Ecto.Query.t()
|
||||
defp medias_for_user_query(user_id) do
|
||||
Media
|
||||
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|
||||
|> join(:inner, [_p, a], u in User, on: a.user_id == u.id)
|
||||
|> where([_p, _a, u], u.id == ^user_id)
|
||||
end
|
||||
end
|
@ -22,8 +22,8 @@ defmodule Mobilizon.Posts.Post do
|
||||
alias Ecto.Changeset
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Tag
|
||||
alias Mobilizon.Media
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Medias
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Posts.Post.TitleSlug
|
||||
alias Mobilizon.Posts.PostVisibility
|
||||
alias Mobilizon.Web.Endpoint
|
||||
@ -41,7 +41,8 @@ defmodule Mobilizon.Posts.Post do
|
||||
publish_at: DateTime.t(),
|
||||
author: Actor.t(),
|
||||
attributed_to: Actor.t(),
|
||||
picture: Picture.t(),
|
||||
picture: Media.t(),
|
||||
media: [Media.t()],
|
||||
tags: [Tag.t()]
|
||||
}
|
||||
|
||||
@ -60,6 +61,7 @@ defmodule Mobilizon.Posts.Post do
|
||||
belongs_to(:attributed_to, Actor)
|
||||
belongs_to(:picture, Picture, on_replace: :update)
|
||||
many_to_many(:tags, Tag, join_through: "posts_tags", on_replace: :delete)
|
||||
many_to_many(:media, Media, join_through: "posts_medias", on_replace: :delete)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
@ -82,6 +84,7 @@ defmodule Mobilizon.Posts.Post do
|
||||
post
|
||||
|> cast(attrs, @attrs)
|
||||
|> maybe_generate_id()
|
||||
|> put_assoc(:media, Map.get(attrs, :media, []))
|
||||
|> put_tags(attrs)
|
||||
|> maybe_put_publish_date()
|
||||
|> put_picture(attrs)
|
||||
@ -146,8 +149,8 @@ defmodule Mobilizon.Posts.Post do
|
||||
# In case the provided picture is an existing one
|
||||
@spec put_picture(Changeset.t(), map) :: Changeset.t()
|
||||
defp put_picture(%Changeset{} = changeset, %{picture: %{picture_id: id} = _picture}) do
|
||||
case Media.get_picture!(id) do
|
||||
%Picture{} = picture ->
|
||||
case Medias.get_media!(id) do
|
||||
%Media{} = picture ->
|
||||
put_assoc(changeset, :picture, picture)
|
||||
|
||||
_ ->
|
||||
|
@ -103,7 +103,7 @@ defmodule Mobilizon.Posts do
|
||||
@spec update_post(Post.t(), map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_post(%Post{} = post, attrs) do
|
||||
post
|
||||
|> Repo.preload(:tags)
|
||||
|> Repo.preload([:tags, :media])
|
||||
|> Post.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
60
lib/service/clean_orphan_media.ex
Normal file
60
lib/service/clean_orphan_media.ex
Normal file
@ -0,0 +1,60 @@
|
||||
defmodule Mobilizon.Service.CleanOrphanMedia do
|
||||
@moduledoc """
|
||||
Service to clean orphan media
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Medias
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Storage.Repo
|
||||
import Ecto.Query
|
||||
|
||||
@grace_period Mobilizon.Config.get([:instance, :orphan_upload_grace_period_hours], 48)
|
||||
|
||||
@doc """
|
||||
Clean orphan media
|
||||
|
||||
Remove media that is not attached to an entity, such as media uploads that were never used in entities.
|
||||
|
||||
Options:
|
||||
* `grace_period` how old in hours can the media be before it's taken into account for deletion
|
||||
* `dry_run` just return the media that would have been deleted, don't actually delete it
|
||||
"""
|
||||
@spec clean(Keyword.t()) :: {:ok, list(Media.t())} | {:error, String.t()}
|
||||
def clean(opts \\ []) do
|
||||
medias = find_media(opts)
|
||||
|
||||
if Keyword.get(opts, :dry_run, false) do
|
||||
{:ok, medias}
|
||||
else
|
||||
Enum.each(medias, fn media ->
|
||||
Medias.delete_media(media, ignore_file_not_found: true)
|
||||
end)
|
||||
|
||||
{:ok, medias}
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_media(Keyword.t()) :: list(Media.t())
|
||||
defp find_media(opts) do
|
||||
grace_period = Keyword.get(opts, :grace_period, @grace_period)
|
||||
expiration_date = DateTime.add(DateTime.utc_now(), grace_period * -3600)
|
||||
|
||||
Media
|
||||
|> where([m], m.inserted_at < ^expiration_date)
|
||||
|> join(:inner, [m], a in Actor)
|
||||
|> where([_m, a], is_nil(a.domain))
|
||||
|> join(:left, [m], e in assoc(m, :events))
|
||||
|> join(:left, [m], ep in assoc(m, :event_picture))
|
||||
|> join(:left, [m], p in assoc(m, :posts))
|
||||
|> join(:left, [m], pp in assoc(m, :posts_picture))
|
||||
|> join(:left, [m], c in assoc(m, :comments))
|
||||
|> where([_m, _a, e], is_nil(e.id))
|
||||
|> where([_m, _a, _e, ep], is_nil(ep.id))
|
||||
|> where([_m, _a, _e, _ep, p], is_nil(p.id))
|
||||
|> where([_m, _a, _e, _ep, _p, pp], is_nil(pp.id))
|
||||
|> where([_m, _a, _e, _ep, _p, _pp, c], is_nil(c.id))
|
||||
|> distinct(true)
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
31
lib/service/workers/clean_orphan_media_worker.ex
Normal file
31
lib/service/workers/clean_orphan_media_worker.ex
Normal file
@ -0,0 +1,31 @@
|
||||
defmodule Mobilizon.Service.Workers.CleanOrphanMediaWorker do
|
||||
@moduledoc """
|
||||
Worker to clean orphan media
|
||||
"""
|
||||
|
||||
use Oban.Worker, queue: "background"
|
||||
alias Mobilizon.Service.CleanOrphanMedia
|
||||
|
||||
@grace_period Mobilizon.Config.get([:instance, :orphan_upload_grace_period_hours], 48)
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{}) do
|
||||
if Mobilizon.Config.get!([:instance, :remove_orphan_uploads]) and should_perform?() do
|
||||
CleanOrphanMedia.clean()
|
||||
end
|
||||
end
|
||||
|
||||
@spec should_perform? :: boolean()
|
||||
defp should_perform? do
|
||||
case Cachex.get(:key_value, "last_media_cleanup") do
|
||||
{:ok, %DateTime{} = last_media_cleanup} ->
|
||||
DateTime.compare(
|
||||
last_media_cleanup,
|
||||
DateTime.add(DateTime.utc_now(), @grace_period * -3600)
|
||||
) == :lt
|
||||
|
||||
_ ->
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
@ -72,7 +72,10 @@ defmodule Mobilizon.Web.Plugs.UploadedMedia do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> send_resp(404, "Not found")
|
||||
|> delete_resp_header("content-disposition")
|
||||
|> put_status(404)
|
||||
|> Phoenix.Controller.put_view(Mobilizon.Web.ErrorView)
|
||||
|> Phoenix.Controller.render("404.html")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
defmodule Mobilizon.Web.Upload.Filter.Optimize do
|
||||
@moduledoc """
|
||||
Handle picture optimizations
|
||||
Handle media optimizations
|
||||
"""
|
||||
|
||||
@behaviour Mobilizon.Web.Upload.Filter
|
||||
|
12
mix.exs
12
mix.exs
@ -236,9 +236,9 @@ defmodule Mobilizon.Mixfile do
|
||||
Mobilizon.Events.Tag.TitleSlug,
|
||||
Mobilizon.Events.Tag.TitleSlug.Type,
|
||||
Mobilizon.Events.TagRelation,
|
||||
Mobilizon.Media,
|
||||
Mobilizon.Media.File,
|
||||
Mobilizon.Media.Picture,
|
||||
Mobilizon.Medias,
|
||||
Mobilizon.Medias.File,
|
||||
Mobilizon.Medias.Media,
|
||||
Mobilizon.Mention,
|
||||
Mobilizon.Reports,
|
||||
Mobilizon.Reports.Note,
|
||||
@ -328,7 +328,7 @@ defmodule Mobilizon.Mixfile do
|
||||
Mobilizon.GraphQL.Resolvers.Group,
|
||||
Mobilizon.GraphQL.Resolvers.Member,
|
||||
Mobilizon.GraphQL.Resolvers.Person,
|
||||
Mobilizon.GraphQL.Resolvers.Picture,
|
||||
Mobilizon.GraphQL.Resolvers.Media,
|
||||
Mobilizon.GraphQL.Resolvers.Report,
|
||||
Mobilizon.GraphQL.Resolvers.Search,
|
||||
Mobilizon.GraphQL.Resolvers.Tag,
|
||||
@ -347,7 +347,7 @@ defmodule Mobilizon.Mixfile do
|
||||
Mobilizon.GraphQL.Schema.EventType,
|
||||
Mobilizon.GraphQL.Schema.Events.FeedTokenType,
|
||||
Mobilizon.GraphQL.Schema.Events.ParticipantType,
|
||||
Mobilizon.GraphQL.Schema.PictureType,
|
||||
Mobilizon.GraphQL.Schema.MediaType,
|
||||
Mobilizon.GraphQL.Schema.ReportType,
|
||||
Mobilizon.GraphQL.Schema.SearchType,
|
||||
Mobilizon.GraphQL.Schema.SortType,
|
||||
@ -374,7 +374,7 @@ defmodule Mobilizon.Mixfile do
|
||||
Mobilizon.Federation.ActivityStream.Converter.Flag,
|
||||
Mobilizon.Federation.ActivityStream.Converter.Follower,
|
||||
Mobilizon.Federation.ActivityStream.Converter.Participant,
|
||||
Mobilizon.Federation.ActivityStream.Converter.Picture,
|
||||
Mobilizon.Federation.ActivityStream.Converter.Media,
|
||||
Mobilizon.Federation.ActivityStream.Converter.Tombstone,
|
||||
Mobilizon.Federation.ActivityStream.Converter.Utils,
|
||||
Mobilizon.Federation.HTTPSignatures.Signature,
|
||||
|
22
priv/repo/migrations/20201124094655_add_media_tables.exs
Normal file
22
priv/repo/migrations/20201124094655_add_media_tables.exs
Normal file
@ -0,0 +1,22 @@
|
||||
defmodule Mobilizon.Storage.Repo.Migrations.AddMediaTables do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
rename(table(:pictures), to: table(:medias))
|
||||
|
||||
create table(:events_medias, primary_key: false) do
|
||||
add(:event_id, references(:events, on_delete: :delete_all), primary_key: true)
|
||||
add(:media_id, references(:medias, on_delete: :delete_all), primary_key: true)
|
||||
end
|
||||
|
||||
create table(:posts_medias, primary_key: false) do
|
||||
add(:post_id, references(:posts, on_delete: :delete_all, type: :uuid), primary_key: true)
|
||||
add(:media_id, references(:medias, on_delete: :delete_all), primary_key: true)
|
||||
end
|
||||
|
||||
create table(:comments_medias, primary_key: false) do
|
||||
add(:comment_id, references(:comments, on_delete: :delete_all), primary_key: true)
|
||||
add(:media_id, references(:medias, on_delete: :delete_all), primary_key: true)
|
||||
end
|
||||
end
|
||||
end
|
@ -526,7 +526,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||
organizer_actor_id: "#{actor.id}",
|
||||
category: "birthday",
|
||||
picture: {
|
||||
picture: {
|
||||
media: {
|
||||
name: "picture for my event",
|
||||
alt: "A very sunny landscape",
|
||||
file: "event.jpg",
|
||||
@ -569,13 +569,13 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||
actor: actor,
|
||||
user: user
|
||||
} do
|
||||
picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
media = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
|
||||
mutation = """
|
||||
mutation { uploadPicture(
|
||||
name: "#{picture.name}",
|
||||
alt: "#{picture.alt}",
|
||||
file: "#{picture.file}"
|
||||
mutation { uploadMedia (
|
||||
name: "#{media.name}",
|
||||
alt: "#{media.alt}",
|
||||
file: "#{media.file}"
|
||||
) {
|
||||
id,
|
||||
url,
|
||||
@ -586,9 +586,9 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||
|
||||
map = %{
|
||||
"query" => mutation,
|
||||
picture.file => %Plug.Upload{
|
||||
media.file => %Plug.Upload{
|
||||
path: "test/fixtures/picture.png",
|
||||
filename: picture.file
|
||||
filename: media.file
|
||||
}
|
||||
}
|
||||
|
||||
@ -601,8 +601,8 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||
map
|
||||
)
|
||||
|
||||
assert json_response(res, 200)["data"]["uploadPicture"]["name"] == picture.name
|
||||
picture_id = json_response(res, 200)["data"]["uploadPicture"]["id"]
|
||||
assert json_response(res, 200)["data"]["uploadMedia"]["name"] == media.name
|
||||
media_id = json_response(res, 200)["data"]["uploadMedia"]["id"]
|
||||
|
||||
mutation = """
|
||||
mutation {
|
||||
@ -615,7 +615,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||
organizer_actor_id: "#{actor.id}",
|
||||
category: "birthday",
|
||||
picture: {
|
||||
picture_id: "#{picture_id}"
|
||||
media_id: "#{media_id}"
|
||||
}
|
||||
) {
|
||||
title,
|
||||
@ -635,7 +635,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||
|
||||
assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event"
|
||||
|
||||
assert json_response(res, 200)["data"]["createEvent"]["picture"]["name"] == picture.name
|
||||
assert json_response(res, 200)["data"]["createEvent"]["picture"]["name"] == media.name
|
||||
|
||||
assert json_response(res, 200)["data"]["createEvent"]["picture"]["url"]
|
||||
end
|
||||
@ -943,7 +943,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||
event_id: #{event.id},
|
||||
category: "birthday",
|
||||
picture: {
|
||||
picture: {
|
||||
media: {
|
||||
name: "picture for my event",
|
||||
alt: "A very sunny landscape",
|
||||
file: "event.jpg",
|
||||
@ -1349,7 +1349,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||
test "delete_event/3 should check the event can be deleted by the user", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
actor: actor
|
||||
actor: _actor
|
||||
} do
|
||||
actor2 = insert(:actor)
|
||||
event = insert(:event, organizer_actor: actor2)
|
||||
|
@ -218,8 +218,8 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do
|
||||
$id: ID!
|
||||
$name: String
|
||||
$summary: String
|
||||
$avatar: PictureInput
|
||||
$banner: PictureInput
|
||||
$avatar: MediaInput
|
||||
$banner: MediaInput
|
||||
$visibility: GroupVisibility
|
||||
$physicalAddress: AddressInput
|
||||
) {
|
||||
|
@ -1,17 +1,17 @@
|
||||
defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
defmodule Mobilizon.GraphQL.Resolvers.MediaTest do
|
||||
use Mobilizon.Web.ConnCase
|
||||
use Bamboo.Test
|
||||
|
||||
import Mobilizon.Factory
|
||||
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Medias.Media
|
||||
|
||||
alias Mobilizon.GraphQL.AbsintheHelpers
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
|
||||
@default_picture_details %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
@default_picture_path "test/fixtures/picture.png"
|
||||
@default_media_details %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
@default_media_path "test/fixtures/picture.png"
|
||||
|
||||
setup %{conn: conn} do
|
||||
user = insert(:user)
|
||||
@ -20,9 +20,9 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
{:ok, conn: conn, user: user, actor: actor}
|
||||
end
|
||||
|
||||
@picture_query """
|
||||
query Picture($id: ID!) {
|
||||
picture(id: $id) {
|
||||
@media_query """
|
||||
query Media($id: ID!) {
|
||||
media(id: $id) {
|
||||
id
|
||||
name,
|
||||
alt,
|
||||
@ -33,9 +33,9 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
}
|
||||
"""
|
||||
|
||||
@upload_picture_mutation """
|
||||
mutation UploadPicture($name: String!, $alt: String, $file: Upload!) {
|
||||
uploadPicture(
|
||||
@upload_media_mutation """
|
||||
mutation UploadMedia($name: String!, $alt: String, $file: Upload!) {
|
||||
uploadMedia(
|
||||
name: $name
|
||||
alt: $alt
|
||||
file: $file
|
||||
@ -48,44 +48,44 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
}
|
||||
"""
|
||||
|
||||
describe "Resolver: Get picture" do
|
||||
test "picture/3 returns the information on a picture", %{conn: conn} do
|
||||
%Picture{id: id} = picture = insert(:picture)
|
||||
describe "Resolver: Get media" do
|
||||
test "media/3 returns the information on a media", %{conn: conn} do
|
||||
%Media{id: id} = media = insert(:media)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(query: @picture_query, variables: %{id: id})
|
||||
|> AbsintheHelpers.graphql_query(query: @media_query, variables: %{id: id})
|
||||
|
||||
assert res["data"]["picture"]["name"] == picture.file.name
|
||||
assert res["data"]["media"]["name"] == media.file.name
|
||||
|
||||
assert res["data"]["picture"]["content_type"] ==
|
||||
picture.file.content_type
|
||||
assert res["data"]["media"]["content_type"] ==
|
||||
media.file.content_type
|
||||
|
||||
assert res["data"]["picture"]["size"] == 13_120
|
||||
assert res["data"]["media"]["size"] == 13_120
|
||||
|
||||
assert res["data"]["picture"]["url"] =~ Endpoint.url()
|
||||
assert res["data"]["media"]["url"] =~ Endpoint.url()
|
||||
end
|
||||
|
||||
test "picture/3 returns nothing on a non-existent picture", %{conn: conn} do
|
||||
test "media/3 returns nothing on a non-existent media", %{conn: conn} do
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(query: @picture_query, variables: %{id: 3})
|
||||
|> AbsintheHelpers.graphql_query(query: @media_query, variables: %{id: 3})
|
||||
|
||||
assert hd(res["errors"])["message"] == "Resource not found"
|
||||
assert hd(res["errors"])["status_code"] == 404
|
||||
end
|
||||
end
|
||||
|
||||
describe "Resolver: Upload picture" do
|
||||
test "upload_picture/3 uploads a new picture", %{conn: conn, user: user} do
|
||||
picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
describe "Resolver: Upload media" do
|
||||
test "upload_media/3 uploads a new media", %{conn: conn, user: user} do
|
||||
media = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
|
||||
map = %{
|
||||
"query" => @upload_picture_mutation,
|
||||
"variables" => picture,
|
||||
picture.file => %Plug.Upload{
|
||||
"query" => @upload_media_mutation,
|
||||
"variables" => media,
|
||||
media.file => %Plug.Upload{
|
||||
path: "test/fixtures/picture.png",
|
||||
filename: picture.file
|
||||
filename: media.file
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,21 +99,21 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
)
|
||||
|> json_response(200)
|
||||
|
||||
assert res["data"]["uploadPicture"]["name"] == picture.name
|
||||
assert res["data"]["uploadPicture"]["content_type"] == "image/png"
|
||||
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||
assert res["data"]["uploadPicture"]["url"]
|
||||
assert res["data"]["uploadMedia"]["name"] == media.name
|
||||
assert res["data"]["uploadMedia"]["content_type"] == "image/png"
|
||||
assert res["data"]["uploadMedia"]["size"] == 10_097
|
||||
assert res["data"]["uploadMedia"]["url"]
|
||||
end
|
||||
|
||||
test "upload_picture/3 forbids uploading if no auth", %{conn: conn} do
|
||||
picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
test "upload_media/3 forbids uploading if no auth", %{conn: conn} do
|
||||
media = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
|
||||
map = %{
|
||||
"query" => @upload_picture_mutation,
|
||||
"variables" => picture,
|
||||
picture.file => %Plug.Upload{
|
||||
"query" => @upload_media_mutation,
|
||||
"variables" => media,
|
||||
media.file => %Plug.Upload{
|
||||
path: "test/fixtures/picture.png",
|
||||
filename: picture.file
|
||||
filename: media.file
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,43 +130,43 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "Resolver: Remove picture" do
|
||||
@remove_picture_mutation """
|
||||
mutation RemovePicture($id: ID!) {
|
||||
removePicture(id: $id) {
|
||||
describe "Resolver: Remove media" do
|
||||
@remove_media_mutation """
|
||||
mutation RemoveMedia($id: ID!) {
|
||||
removeMedia(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "Removes a previously uploaded picture", %{conn: conn, user: user, actor: actor} do
|
||||
%Picture{id: picture_id} = insert(:picture, actor: actor)
|
||||
test "Removes a previously uploaded media", %{conn: conn, user: user, actor: actor} do
|
||||
%Media{id: media_id} = insert(:media, actor: actor)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @remove_picture_mutation,
|
||||
variables: %{id: picture_id}
|
||||
query: @remove_media_mutation,
|
||||
variables: %{id: media_id}
|
||||
)
|
||||
|
||||
assert is_nil(res["errors"])
|
||||
assert res["data"]["removePicture"]["id"] == to_string(picture_id)
|
||||
assert res["data"]["removeMedia"]["id"] == to_string(media_id)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(query: @picture_query, variables: %{id: picture_id})
|
||||
|> AbsintheHelpers.graphql_query(query: @media_query, variables: %{id: media_id})
|
||||
|
||||
assert hd(res["errors"])["message"] == "Resource not found"
|
||||
assert hd(res["errors"])["status_code"] == 404
|
||||
end
|
||||
|
||||
test "Removes nothing if picture is not found", %{conn: conn, user: user} do
|
||||
test "Removes nothing if media is not found", %{conn: conn, user: user} do
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @remove_picture_mutation,
|
||||
query: @remove_media_mutation,
|
||||
variables: %{id: 400}
|
||||
)
|
||||
|
||||
@ -174,14 +174,14 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
assert hd(res["errors"])["status_code"] == 404
|
||||
end
|
||||
|
||||
test "Removes nothing if picture if not logged-in", %{conn: conn, actor: actor} do
|
||||
%Picture{id: picture_id} = insert(:picture, actor: actor)
|
||||
test "Removes nothing if media if not logged-in", %{conn: conn, actor: actor} do
|
||||
%Media{id: media_id} = insert(:media, actor: actor)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @remove_picture_mutation,
|
||||
variables: %{id: picture_id}
|
||||
query: @remove_media_mutation,
|
||||
variables: %{id: media_id}
|
||||
)
|
||||
|
||||
assert hd(res["errors"])["message"] == "You need to be logged in"
|
||||
@ -210,8 +210,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
|
||||
assert res["data"]["loggedPerson"]["mediaSize"] == 0
|
||||
|
||||
res = upload_picture(conn, user)
|
||||
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||
res = upload_media(conn, user)
|
||||
assert res["data"]["uploadMedia"]["size"] == 10_097
|
||||
|
||||
res =
|
||||
conn
|
||||
@ -221,14 +221,14 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
assert res["data"]["loggedPerson"]["mediaSize"] == 10_097
|
||||
|
||||
res =
|
||||
upload_picture(
|
||||
upload_media(
|
||||
conn,
|
||||
user,
|
||||
"test/fixtures/image.jpg",
|
||||
Map.put(@default_picture_details, :file, "image.jpg")
|
||||
Map.put(@default_media_details, :file, "image.jpg")
|
||||
)
|
||||
|
||||
assert res["data"]["uploadPicture"]["size"] == 13_227
|
||||
assert res["data"]["uploadMedia"]["size"] == 13_227
|
||||
|
||||
res =
|
||||
conn
|
||||
@ -266,7 +266,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
assert is_nil(res["errors"])
|
||||
assert hd(res["data"]["persons"]["elements"])["mediaSize"] == 0
|
||||
|
||||
upload_picture(conn, user)
|
||||
upload_media(conn, user)
|
||||
|
||||
res =
|
||||
conn
|
||||
@ -355,8 +355,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
assert res["errors"] == nil
|
||||
assert res["data"]["loggedUser"]["mediaSize"] == 0
|
||||
|
||||
res = upload_picture(conn, user)
|
||||
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||
res = upload_media(conn, user)
|
||||
assert res["data"]["uploadMedia"]["size"] == 10_097
|
||||
|
||||
res =
|
||||
conn
|
||||
@ -366,14 +366,14 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
assert res["data"]["loggedUser"]["mediaSize"] == 10_097
|
||||
|
||||
res =
|
||||
upload_picture(
|
||||
upload_media(
|
||||
conn,
|
||||
user,
|
||||
"test/fixtures/image.jpg",
|
||||
Map.put(@default_picture_details, :file, "image.jpg")
|
||||
Map.put(@default_media_details, :file, "image.jpg")
|
||||
)
|
||||
|
||||
assert res["data"]["uploadPicture"]["size"] == 13_227
|
||||
assert res["data"]["uploadMedia"]["size"] == 13_227
|
||||
|
||||
res =
|
||||
conn
|
||||
@ -393,14 +393,14 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
assert is_nil(res["errors"])
|
||||
|
||||
res =
|
||||
upload_picture(
|
||||
upload_media(
|
||||
conn,
|
||||
user,
|
||||
"test/fixtures/image.jpg",
|
||||
Map.put(@default_picture_details, :file, "image.jpg")
|
||||
Map.put(@default_media_details, :file, "image.jpg")
|
||||
)
|
||||
|
||||
assert res["data"]["uploadPicture"]["size"] == 13_227
|
||||
assert res["data"]["uploadMedia"]["size"] == 13_227
|
||||
|
||||
res =
|
||||
conn
|
||||
@ -438,9 +438,9 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
assert is_nil(res["errors"])
|
||||
assert hd(res["data"]["users"]["elements"])["mediaSize"] == 0
|
||||
|
||||
res = upload_picture(conn, user)
|
||||
res = upload_media(conn, user)
|
||||
assert is_nil(res["errors"])
|
||||
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||
assert res["data"]["uploadMedia"]["size"] == 10_097
|
||||
|
||||
res =
|
||||
conn
|
||||
@ -463,19 +463,19 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||
end
|
||||
end
|
||||
|
||||
@spec upload_picture(Plug.Conn.t(), Mobilizon.Users.User.t(), String.t(), map()) :: map()
|
||||
defp upload_picture(
|
||||
@spec upload_media(Plug.Conn.t(), Mobilizon.Users.User.t(), String.t(), map()) :: map()
|
||||
defp upload_media(
|
||||
conn,
|
||||
user,
|
||||
picture_path \\ @default_picture_path,
|
||||
picture_details \\ @default_picture_details
|
||||
media_path \\ @default_media_path,
|
||||
media_details \\ @default_media_details
|
||||
) do
|
||||
map = %{
|
||||
"query" => @upload_picture_mutation,
|
||||
"variables" => picture_details,
|
||||
picture_details.file => %Plug.Upload{
|
||||
path: picture_path,
|
||||
filename: picture_details.file
|
||||
"query" => @upload_media_mutation,
|
||||
"variables" => media_details,
|
||||
media_details.file => %Plug.Upload{
|
||||
path: media_path,
|
||||
filename: media_details.file
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||
name: "secret person",
|
||||
summary: "no-one will know who I am",
|
||||
banner: {
|
||||
picture: {
|
||||
media: {
|
||||
file: "landscape.jpg",
|
||||
name: "irish landscape",
|
||||
alt: "The beautiful atlantic way"
|
||||
@ -274,7 +274,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||
name: "riri updated",
|
||||
summary: "summary updated",
|
||||
banner: {
|
||||
picture: {
|
||||
media: {
|
||||
file: "landscape.jpg",
|
||||
name: "irish landscape",
|
||||
alt: "The beautiful atlantic way"
|
||||
|
@ -9,7 +9,7 @@ defmodule Mobilizon.ActorsTest do
|
||||
alias Mobilizon.Actors.{Actor, Bot, Follower, Member}
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Media.File, as: FileModel
|
||||
alias Mobilizon.Medias.File, as: FileModel
|
||||
alias Mobilizon.Service.Workers
|
||||
alias Mobilizon.Storage.Page
|
||||
|
||||
|
@ -58,7 +58,8 @@ defmodule Mobilizon.DiscussionsTest do
|
||||
%Comment{} = comment = insert(:comment)
|
||||
assert {:error, %Ecto.Changeset{}} = Discussions.update_comment(comment, @invalid_attrs)
|
||||
%Comment{} = comment_fetched = Discussions.get_comment!(comment.id)
|
||||
assert comment = comment_fetched
|
||||
assert comment.text == comment_fetched.text
|
||||
assert comment.url == comment_fetched.url
|
||||
end
|
||||
|
||||
test "delete_comment/1 deletes the comment" do
|
||||
|
@ -292,7 +292,7 @@ defmodule Mobilizon.EventsTest do
|
||||
tag1: %Tag{id: tag1_id} = tag1,
|
||||
tag2: %Tag{id: tag2_id} = tag2
|
||||
} do
|
||||
assert {:ok, %TagRelation{} = tag_relation} =
|
||||
assert {:ok, %TagRelation{}} =
|
||||
Events.create_tag_relation(%{tag_id: tag1_id, link_id: tag2_id})
|
||||
|
||||
assert Events.are_tags_linked(tag1, tag2)
|
||||
|
@ -3,13 +3,13 @@ defmodule Mobilizon.MediaTest do
|
||||
|
||||
import Mobilizon.Factory
|
||||
|
||||
alias Mobilizon.{Config, Media}
|
||||
alias Mobilizon.{Config, Medias}
|
||||
|
||||
alias Mobilizon.Web.Upload.Uploader
|
||||
|
||||
describe "media" do
|
||||
setup [:ensure_local_uploader]
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Medias.Media
|
||||
|
||||
@valid_attrs %{
|
||||
file: %{
|
||||
@ -24,39 +24,42 @@ defmodule Mobilizon.MediaTest do
|
||||
}
|
||||
}
|
||||
|
||||
test "get_picture!/1 returns the picture with given id" do
|
||||
picture = insert(:picture)
|
||||
assert Media.get_picture!(picture.id).id == picture.id
|
||||
test "get_media!/1 returns the media with given id" do
|
||||
media = insert(:media)
|
||||
assert Medias.get_media!(media.id).id == media.id
|
||||
end
|
||||
|
||||
test "create_picture/1 with valid data creates a picture" do
|
||||
assert {:ok, %Picture{} = picture} =
|
||||
Media.create_picture(Map.put(@valid_attrs, :actor_id, insert(:actor).id))
|
||||
test "create_media/1 with valid data creates a media" do
|
||||
assert {:ok, %Media{} = media} =
|
||||
Medias.create_media(Map.put(@valid_attrs, :actor_id, insert(:actor).id))
|
||||
|
||||
assert picture.file.name == "something old"
|
||||
assert media.file.name == "something old"
|
||||
end
|
||||
|
||||
test "update_picture/2 with valid data updates the picture" do
|
||||
picture = insert(:picture)
|
||||
test "update_media/2 with valid data updates the media" do
|
||||
media = insert(:media)
|
||||
|
||||
assert {:ok, %Picture{} = picture} =
|
||||
Media.update_picture(picture, Map.put(@update_attrs, :actor_id, insert(:actor).id))
|
||||
assert {:ok, %Media{} = media} =
|
||||
Medias.update_media(
|
||||
media,
|
||||
Map.put(@update_attrs, :actor_id, insert(:actor).id)
|
||||
)
|
||||
|
||||
assert picture.file.name == "something new"
|
||||
assert media.file.name == "something new"
|
||||
end
|
||||
|
||||
test "delete_picture/1 deletes the picture" do
|
||||
picture = insert(:picture)
|
||||
test "delete_media/1 deletes the media" do
|
||||
media = insert(:media)
|
||||
|
||||
%URI{path: "/media/" <> path} = URI.parse(picture.file.url)
|
||||
%URI{path: "/media/" <> path} = URI.parse(media.file.url)
|
||||
|
||||
assert File.exists?(
|
||||
Config.get!([Uploader.Local, :uploads]) <>
|
||||
"/" <> path
|
||||
)
|
||||
|
||||
assert {:ok, %Picture{}} = Media.delete_picture(picture)
|
||||
assert_raise Ecto.NoResultsError, fn -> Media.get_picture!(picture.id) end
|
||||
assert {:ok, %Media{}} = Medias.delete_media(media)
|
||||
assert_raise Ecto.NoResultsError, fn -> Medias.get_media!(media.id) end
|
||||
|
||||
refute File.exists?(
|
||||
Config.get!([Uploader.Local, :uploads]) <>
|
||||
|
@ -70,7 +70,7 @@ defmodule Mobilizon.PostsTest do
|
||||
%Post{} = post = insert(:post)
|
||||
assert {:error, %Ecto.Changeset{}} = Posts.update_post(post, @invalid_attrs)
|
||||
%Post{} = post_fetched = Posts.get_post(post.id)
|
||||
assert post = post_fetched
|
||||
assert post.body == post_fetched.body
|
||||
end
|
||||
|
||||
test "delete_post/1 deletes the post" do
|
||||
|
56
test/service/clean_orphan_media_test.exs
Normal file
56
test/service/clean_orphan_media_test.exs
Normal file
@ -0,0 +1,56 @@
|
||||
defmodule Mix.Tasks.Mobilizon.Media.CleanOrphanTest do
|
||||
use Mobilizon.DataCase
|
||||
|
||||
import Mobilizon.Factory
|
||||
|
||||
alias Mobilizon.Medias
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Service.CleanOrphanMedia
|
||||
|
||||
describe "clean orphan media" do
|
||||
test "with default values" do
|
||||
{:ok, old, _} = DateTime.from_iso8601("2020-11-20T17:35:23+01:00")
|
||||
%Media{id: media_id} = insert(:media, inserted_at: old)
|
||||
%Media{id: media_2_id} = insert(:media)
|
||||
|
||||
refute is_nil(Medias.get_media(media_id))
|
||||
refute is_nil(Medias.get_media(media_2_id))
|
||||
|
||||
assert {:ok, [found_media]} = CleanOrphanMedia.clean()
|
||||
assert found_media.id == media_id
|
||||
|
||||
assert is_nil(Medias.get_media(media_id))
|
||||
refute is_nil(Medias.get_media(media_2_id))
|
||||
end
|
||||
|
||||
test "as dry-run" do
|
||||
{:ok, old, _} = DateTime.from_iso8601("2020-11-20T17:35:23+01:00")
|
||||
%Media{id: media_id} = insert(:media, inserted_at: old)
|
||||
%Media{id: media_2_id} = insert(:media)
|
||||
|
||||
refute is_nil(Medias.get_media(media_id))
|
||||
refute is_nil(Medias.get_media(media_2_id))
|
||||
|
||||
assert {:ok, [found_media]} = CleanOrphanMedia.clean(dry_run: true)
|
||||
assert found_media.id == media_id
|
||||
|
||||
refute is_nil(Medias.get_media(media_id))
|
||||
refute is_nil(Medias.get_media(media_2_id))
|
||||
end
|
||||
|
||||
test "with custom grace period" do
|
||||
date = DateTime.utc_now() |> DateTime.add(24 * -3600)
|
||||
%Media{id: media_id} = insert(:media, inserted_at: date)
|
||||
%Media{id: media_2_id} = insert(:media)
|
||||
|
||||
refute is_nil(Medias.get_media(media_id))
|
||||
refute is_nil(Medias.get_media(media_2_id))
|
||||
|
||||
assert {:ok, [found_media]} = CleanOrphanMedia.clean(grace_period: 12)
|
||||
assert found_media.id == media_id
|
||||
|
||||
assert is_nil(Medias.get_media(media_id))
|
||||
refute is_nil(Medias.get_media(media_2_id))
|
||||
end
|
||||
end
|
||||
end
|
@ -148,6 +148,7 @@ defmodule Mobilizon.Factory do
|
||||
event: build(:event),
|
||||
uuid: uuid,
|
||||
mentions: [],
|
||||
media: [],
|
||||
attributed_to: nil,
|
||||
local: true,
|
||||
deleted_at: nil,
|
||||
@ -179,13 +180,14 @@ defmodule Mobilizon.Factory do
|
||||
local: true,
|
||||
publish_at: DateTime.utc_now(),
|
||||
url: Routes.page_url(Endpoint, :event, uuid),
|
||||
picture: insert(:picture),
|
||||
picture: insert(:media),
|
||||
uuid: uuid,
|
||||
join_options: :free,
|
||||
options: %{},
|
||||
participant_stats: %{},
|
||||
status: :confirmed,
|
||||
contacts: []
|
||||
contacts: [],
|
||||
media: []
|
||||
}
|
||||
end
|
||||
|
||||
@ -269,16 +271,16 @@ defmodule Mobilizon.Factory do
|
||||
size: 13_227
|
||||
} = data
|
||||
|
||||
%Mobilizon.Media.File{
|
||||
name: "My Picture",
|
||||
%Mobilizon.Medias.File{
|
||||
name: "My Media",
|
||||
url: url,
|
||||
content_type: "image/png",
|
||||
size: 13_120
|
||||
}
|
||||
end
|
||||
|
||||
def picture_factory do
|
||||
%Mobilizon.Media.Picture{
|
||||
def media_factory do
|
||||
%Mobilizon.Medias.Media{
|
||||
file: build(:file),
|
||||
actor: build(:actor)
|
||||
}
|
||||
@ -372,6 +374,7 @@ defmodule Mobilizon.Factory do
|
||||
tags: build_list(3, :tag),
|
||||
visibility: :public,
|
||||
publish_at: DateTime.utc_now(),
|
||||
media: [],
|
||||
url: Routes.page_url(Endpoint, :post, uuid)
|
||||
}
|
||||
end
|
||||
|
124
test/tasks/media/clean_orphan_test.exs
Normal file
124
test/tasks/media/clean_orphan_test.exs
Normal file
@ -0,0 +1,124 @@
|
||||
defmodule Mix.Tasks.Mobilizon.Media.CleanOrphanTest do
|
||||
use Mobilizon.DataCase
|
||||
import Mock
|
||||
import Mobilizon.Factory
|
||||
|
||||
alias Mix.Tasks.Mobilizon.Media.CleanOrphan
|
||||
alias Mobilizon.Service.CleanOrphanMedia
|
||||
|
||||
Mix.shell(Mix.Shell.Process)
|
||||
|
||||
describe "with default options" do
|
||||
test "nothing returned" do
|
||||
with_mock CleanOrphanMedia, clean: fn [dry_run: false, grace_period: 48] -> {:ok, []} end do
|
||||
CleanOrphan.run([])
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "No files were deleted"
|
||||
end
|
||||
end
|
||||
|
||||
test "media returned" do
|
||||
media1 = insert(:media)
|
||||
media2 = insert(:media)
|
||||
|
||||
with_mock CleanOrphanMedia,
|
||||
clean: fn [dry_run: false, grace_period: 48] -> {:ok, [media1, media2]} end do
|
||||
CleanOrphan.run([])
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "2 files have been deleted"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with dry-run option" do
|
||||
test "with nothing returned" do
|
||||
with_mock CleanOrphanMedia,
|
||||
clean: fn [dry_run: true, grace_period: 48] -> {:ok, []} end do
|
||||
CleanOrphan.run(["--dry-run"])
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "No files would have been deleted"
|
||||
end
|
||||
end
|
||||
|
||||
test "with media returned" do
|
||||
media1 = insert(:media)
|
||||
media2 = insert(:media)
|
||||
|
||||
with_mock CleanOrphanMedia,
|
||||
clean: fn [dry_run: true, grace_period: 48] -> {:ok, [media1, media2]} end do
|
||||
CleanOrphan.run(["--dry-run"])
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "List of files that would have been deleted"
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
|
||||
assert output_received ==
|
||||
"ID: #{media1.id}, Actor: #{media1.actor_id}, URL: #{media1.file.url}"
|
||||
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
|
||||
assert output_received ==
|
||||
"ID: #{media2.id}, Actor: #{media2.actor_id}, URL: #{media2.file.url}"
|
||||
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "2 files would have been deleted"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with verbose option" do
|
||||
test "with nothing returned" do
|
||||
with_mock CleanOrphanMedia,
|
||||
clean: fn [dry_run: false, grace_period: 48] -> {:ok, []} end do
|
||||
CleanOrphan.run(["--verbose"])
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "No files were deleted"
|
||||
end
|
||||
end
|
||||
|
||||
test "with media returned" do
|
||||
media1 = insert(:media)
|
||||
media2 = insert(:media)
|
||||
|
||||
with_mock CleanOrphanMedia,
|
||||
clean: fn [dry_run: false, grace_period: 48] -> {:ok, [media1, media2]} end do
|
||||
CleanOrphan.run(["--verbose"])
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "List of files that have been deleted"
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
|
||||
assert output_received ==
|
||||
"ID: #{media1.id}, Actor: #{media1.actor_id}, URL: #{media1.file.url}"
|
||||
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
|
||||
assert output_received ==
|
||||
"ID: #{media2.id}, Actor: #{media2.actor_id}, URL: #{media2.file.url}"
|
||||
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "2 files have been deleted"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with days option" do
|
||||
test "with nothing returned" do
|
||||
with_mock CleanOrphanMedia,
|
||||
clean: fn [dry_run: false, grace_period: 120] -> {:ok, []} end do
|
||||
CleanOrphan.run(["--days", "5"])
|
||||
assert_received {:mix_shell, :info, [output_received]}
|
||||
assert output_received == "No files were deleted"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "returns an error" do
|
||||
test "for some reason" do
|
||||
with_mock CleanOrphanMedia,
|
||||
clean: fn [dry_run: false, grace_period: 48] -> {:error, "Some error"} end do
|
||||
CleanOrphan.run([])
|
||||
assert_received {:mix_shell, :error, [output_received]}
|
||||
assert output_received == "Error while cleaning orphan media files"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user