Skip to content

Commit

Permalink
Refactored api controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
benfortuna committed Jan 28, 2024
1 parent d7b51eb commit 5576202
Show file tree
Hide file tree
Showing 72 changed files with 3,439 additions and 3 deletions.
41 changes: 41 additions & 0 deletions src/main/java/org/coucal/api/ControllerBinder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2023 Ben Fortuna
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.coucal.api;

import net.fortuna.ical4j.util.RandomUidGenerator;
import net.fortuna.ical4j.util.UidGenerator;
import org.coucal.core.RepositoryManager;
import org.coucal.core.WorkspaceManager;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.ical4j.connector.CalendarStore;
import org.ical4j.connector.CardStore;
import org.ical4j.connector.local.LocalCalendarStore;
import org.ical4j.connector.local.LocalCardStore;

import java.io.File;

public class ControllerBinder extends AbstractBinder {

@Override
protected void configure() {
bind(RepositoryManager.class).to(RepositoryManager.class);
bind(WorkspaceManager.class).to(WorkspaceManager.class);
bind(new LocalCalendarStore(new File("build/calendar/store"))).to(CalendarStore.class);
bind(new LocalCardStore(new File("build/card/store"))).to(CardStore.class);
bind(RandomUidGenerator.class).to(UidGenerator.class);
}
}
105 changes: 105 additions & 0 deletions src/main/java/org/coucal/api/controller/channel/ChannelOperations.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.coucal.api.controller.channel;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.vcard.VCard;
import org.coucal.api.controller.ICalendarResponseVariants;

import java.util.Collections;

/**
* Channels control the ingress and egress of content stored in repositories. Specifically channels can transform
* content to alternate formats for publishing and conform with update mechanisms defined externally.
*
* Channels are managed globally, however each channel definition is specifically linked to a single repository. This
* ensures the internal content structure is not leaked externally, but external updates can still be linked back to
* the corresponding repository.
*/
public interface ChannelOperations extends ICalendarResponseVariants {

@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Operation(summary = "List channels",
description = "List channel identifiers")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Operation successful"),
@ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
})
default Response listChannels(@Context Request req) {
return ok(Collections::emptyList, req);
}

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Create a new channel",
description = "Create a new channel from the specified identifier")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Successfully created"),
@ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
})
default Response createChannel(VCard channel, @Context Request req) {
return ok(() -> channel, req);
}

@GET
@Path("{uid}")
@Operation(summary = "Retrieve a single channel",
description = "Return the latest revision of the channel object with the specified UID property")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Operation successful"),
@ApiResponse(responseCode = "404", description = "Not found - The channel was not found")
})
default Response getChannel(@PathParam("uid") String uid, @Context Request req) {
VCard channel = new VCard().add(new Uid(uid));
return ok(() -> channel, req);
}

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("{uid}")
@Operation(summary = "Create a new revision of an existing channel object",
description = "Create a new revision of an existing channel with the specified UID property.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully updated"),
@ApiResponse(responseCode = "404", description = "Not found - The channel was not found")
})
default Response updateChannel(@PathParam("uid") String uid, VCard channelMods, @Context Request req) {
channelMods.add(new Uid(uid));
return ok(() -> channelMods, req);
}

// @PUT
// @Consumes(MediaType.APPLICATION_JSON)
// @Path("{uid}")
// @Operation(summary = "Overwrite a channel object",
// description = "Replace all revisions of a channel (if it exists) with the specified UID property" +
// " using the specified payload")
// @ApiResponses(value = {
// @ApiResponse(responseCode = "200", description = "Successfully replaced"),
// @ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
// })
default Response setChannel(@PathParam("uid") String uid, VCard channel, @Context Request req) {
channel.add(new Uid(uid));
return ok(() -> channel, req);
}

@DELETE
@Path("{uid}")
@Operation(summary = "Delete a channel object",
description = "Remove all revisions of an existing channel with the specified UID property.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully deleted"),
@ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
})
default Response deleteChannel(@PathParam("uid") String uid, @Context Request req) {
VCard channel = new VCard().add(new Uid(uid));
return ok(() -> channel, req);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.coucal.api.controller.channel;

import jakarta.ws.rs.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("/channels")
public class ChannelsController implements ChannelOperations {

private static final String CHANNELS_COLLECTION = "channels";

private static final String CHANNELS_WORKSPACE = "system";

private static final Logger LOGGER = LoggerFactory.getLogger(ChannelsController.class);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.coucal.api.controller.channel.chat;

import org.coucal.api.controller.ICalendarResponseVariants;

public interface ChatOperations extends ICalendarResponseVariants {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.coucal.api.controller.channel.email;

import jakarta.ws.rs.Path;

@Path("/channels/{channel}/email")
public class EmailQuarantineController implements EmailQuarantineOperations {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.coucal.api.controller.channel.email;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response;
import org.coucal.api.controller.ICalendarResponseVariants;

import java.util.Collections;

/**
* An Email channel differs from other channels in that content is not transferred via HTTP, but via SMTP and
* other Email-related protocols. However, Email channels still require some API functionality for triggering
* send, receive and other workflows.
*
* Also unlike most channel types, Email channels require an approval step for specific content updates, such as
* registering new events. This is required to combat potential spam content from unknown sources.
*/
public interface EmailQuarantineOperations extends ICalendarResponseVariants {

@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Operation(summary = "List quarantine queue",
description = "List content that has been quarantined pending approval")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Operation successful"),
@ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
})
default Response listQuarantineQueue(@PathParam("channel") String channel, @Context Request req) {
return ok(Collections::emptyList, req);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.coucal.api.controller.channel.webhooks;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response;
import net.fortuna.ical4j.model.component.VEvent;
import org.coucal.api.controller.ICalendarResponseVariants;

/**
* An IFTTT Webhook supports requests and updates from an "If this, then that (IFTTT)" applet.
*
* An IFTTT Webhook requires authentication so that content is implicitly verified and can be applied to the
* target repository without an approval step.
*/
public interface IFTTTWebhookOperations extends ICalendarResponseVariants {

@POST
@Path("create_event")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Create a new event",
description = "Create a new event from an IFTT invocation")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Success"),
@ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
})
default Response createEvent(VEvent event, @Context Request req) {
return ok(() -> null, req);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.coucal.api.controller.channel.webhooks;

import jakarta.ws.rs.Path;

@Path("/channels/{channel}/ifttt/v1/actions")
public class IFTTTWebhooksController implements IFTTTWebhookOperations {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.coucal.api.controller.channel.webmention;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response;
import net.fortuna.ical4j.model.component.VToDo;
import net.fortuna.ical4j.model.property.Status;
import net.fortuna.ical4j.model.property.Uid;
import org.coucal.api.controller.ICalendarResponseVariants;

import java.util.Collections;

/**
* When an external site registers a new Webmention via a POST call to the registration endpoint, a new
* VTODO object is created with concept LINK_REGISTRATION. Periodically the repository owner(s) should check
* for new registrations and approve or reject accordingly.
*
* Upon approval the content corresponding to the source URL (i.e. has a matching URL property) will be updated
* with a new LINK property of type "replies".
*
* A Webmention channel differs from most other channels in that it requires an approval step to apply incoming
* changes. This is because the endpoint is unauthenticated and the validity of link registrations must be
* verified.
*/
public interface WebMentionOperations extends ICalendarResponseVariants {

@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Operation(summary = "List link registrations",
description = "List link registrations for the specified repository")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Operation successful"),
@ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
})
default Response listLinkRegistrations(@PathParam("channel") String channel, @Context Request req) {
return ok(Collections::emptyList, req);
}

@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Operation(summary = "Register a Webmention link",
description = "Create a new link from the specified form data")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Successfully created"),
@ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
})
default Response registerLink(@PathParam("channel") String channel, @FormParam("source") String source,
@FormParam("target") String target, @Context Request req) {
VToDo linkRegistration = new VToDo();
return ok(() -> linkRegistration, req);
}

@GET
@Path("{uid}")
@Operation(summary = "Retrieve a single link registration",
description = "Return the latest revision of the link registration with the specified UID property")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Operation successful"),
@ApiResponse(responseCode = "404", description = "Not found - The registration was not found")
})
default Response getLinkRegistration(@PathParam("channel") String channel, @PathParam("uid") String uid,
@Context Request req) {
VToDo linkRegistration = new VToDo().withProperty(new Uid(uid)).getFluentTarget();
return ok(() -> linkRegistration, req);
}

@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path("{uid}")
@Operation(summary = "Create a new revision of an existing link registration",
description = "Create a new revision of an existing link registration with the specified UID property.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully updated"),
@ApiResponse(responseCode = "404", description = "Not found - The link registration was not found")
})
default Response updateLinkRegistrationStatus(@PathParam("channel") String channel, @PathParam("uid") String uid,
@FormParam("status") String status, @Context Request req) {
VToDo linkRegistration = new VToDo().add(new Uid(uid)).replace(new Status(status));
return ok(() -> linkRegistration, req);
}

@DELETE
@Path("{uid}")
@Operation(summary = "Delete a link registration",
description = "Remove all revisions of an existing link registration with the specified UID property.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully deleted"),
@ApiResponse(responseCode = "404", description = "Not found - The repository was not found")
})
default Response deleteLinkRegistration(@PathParam("channel") String channel, @PathParam("uid") String uid,
@Context Request req) {
VToDo linkRegistration = new VToDo().withProperty(new Uid(uid)).getFluentTarget();
return ok(() -> linkRegistration, req);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.coucal.api.controller.channel.webmention;

import jakarta.ws.rs.Path;

@Path("/channels/{channel}/webmention")
public class WebMentionsController implements WebMentionOperations {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.coucal.api.controller.channel.websub;

/**
* Responsible for subscription management such as subscribe/unsubscribe requests.
*/
public class HubController {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.coucal.api.controller.channel.websub;

/**
* Responsible for managing incoming content distribution from active subscriptions.
*/
public class SubController {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.coucal.api.controller.channel.websub;

import org.coucal.api.controller.ICalendarResponseVariants;

public interface WebSubOperations extends ICalendarResponseVariants {
}
Loading

0 comments on commit 5576202

Please sign in to comment.