forked from Archives/Athou_commafeed
use an annotation processor instead of a groovy script because of classloading issues
This commit is contained in:
36
pom.xml
36
pom.xml
@@ -138,6 +138,19 @@
|
|||||||
<outputDirectory>target/generated-sources/metamodel</outputDirectory>
|
<outputDirectory>target/generated-sources/metamodel</outputDirectory>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>doc</id>
|
||||||
|
<phase>process-classes</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>process</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<processors>
|
||||||
|
<processor>com.commafeed.frontend.APIGenerator</processor>
|
||||||
|
</processors>
|
||||||
|
<outputDirectory>target/generated-sources/api-docs</outputDirectory>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -340,6 +353,12 @@
|
|||||||
<artifactId>swagger-annotations_2.9.1</artifactId>
|
<artifactId>swagger-annotations_2.9.1</artifactId>
|
||||||
<version>1.2.5</version>
|
<version>1.2.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.wordnik</groupId>
|
||||||
|
<artifactId>swagger-jaxrs_2.9.1</artifactId>
|
||||||
|
<version>1.2.5</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
@@ -471,15 +490,10 @@
|
|||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
<version>2.4</version>
|
<version>2.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.wordnik</groupId>
|
|
||||||
<artifactId>swagger-jaxrs_2.9.1</artifactId>
|
|
||||||
<version>1.2.5</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<phase>generate-sources</phase>
|
<phase>process-classes</phase>
|
||||||
<goals>
|
<goals>
|
||||||
<goal>execute</goal>
|
<goal>execute</goal>
|
||||||
</goals>
|
</goals>
|
||||||
@@ -489,7 +503,6 @@
|
|||||||
<prefix>templates/</prefix>
|
<prefix>templates/</prefix>
|
||||||
<destination>${basedir}/target/generated-sources/angularjs/all-templates.html</destination>
|
<destination>${basedir}/target/generated-sources/angularjs/all-templates.html</destination>
|
||||||
<i18nPath>${basedir}/src/main/resources/i18n/</i18nPath>
|
<i18nPath>${basedir}/src/main/resources/i18n/</i18nPath>
|
||||||
<doc.path>${basedir}/target/generated-sources/swagger-doc</doc.path>
|
|
||||||
</properties>
|
</properties>
|
||||||
<scriptpath>
|
<scriptpath>
|
||||||
<element>${basedir}/src/main/script</element>
|
<element>${basedir}/src/main/script</element>
|
||||||
@@ -505,11 +518,6 @@
|
|||||||
new
|
new
|
||||||
HTMLConcat().concat(source,
|
HTMLConcat().concat(source,
|
||||||
prefix, dest, i18n);
|
prefix, dest, i18n);
|
||||||
|
|
||||||
def docPath =
|
|
||||||
project.properties['doc.path'];
|
|
||||||
new
|
|
||||||
SwaggerStaticGenerator().generate(docPath);
|
|
||||||
</source>
|
</source>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
@@ -545,8 +553,8 @@
|
|||||||
</includes>
|
</includes>
|
||||||
</resource>
|
</resource>
|
||||||
<resource>
|
<resource>
|
||||||
<directory>target/generated-sources/swagger-doc/</directory>
|
<directory>target/generated-sources/api-docs/</directory>
|
||||||
<targetPath>api/swagger-doc</targetPath>
|
<targetPath>api/api-docs</targetPath>
|
||||||
<includes>
|
<includes>
|
||||||
<include>**/*</include>
|
<include>**/*</include>
|
||||||
</includes>
|
</includes>
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ public class FeedDAO extends GenericDAO<Feed> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@XmlRootElement
|
||||||
public static enum DuplicateMode {
|
public static enum DuplicateMode {
|
||||||
NORMALIZED_URL(Feed_.normalizedUrlHash), LAST_CONTENT(Feed_.lastContentHash), PUSH_TOPIC(Feed_.pushTopicHash);
|
NORMALIZED_URL(Feed_.normalizedUrlHash), LAST_CONTENT(Feed_.lastContentHash), PUSH_TOPIC(Feed_.pushTopicHash);
|
||||||
private SingularAttribute<Feed, String> path;
|
private SingularAttribute<Feed, String> path;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import javax.persistence.JoinColumn;
|
|||||||
import javax.persistence.Lob;
|
import javax.persistence.Lob;
|
||||||
import javax.persistence.OneToOne;
|
import javax.persistence.OneToOne;
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
|
||||||
import org.hibernate.annotations.Cache;
|
import org.hibernate.annotations.Cache;
|
||||||
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||||
@@ -21,14 +22,17 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
|
|||||||
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
|
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
|
||||||
public class UserSettings extends AbstractModel {
|
public class UserSettings extends AbstractModel {
|
||||||
|
|
||||||
|
@XmlRootElement
|
||||||
public enum ReadingMode {
|
public enum ReadingMode {
|
||||||
all, unread
|
all, unread
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@XmlRootElement
|
||||||
public enum ReadingOrder {
|
public enum ReadingOrder {
|
||||||
asc, desc
|
asc, desc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@XmlRootElement
|
||||||
public enum ViewMode {
|
public enum ViewMode {
|
||||||
title, expanded
|
title, expanded
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/main/java/com/commafeed/frontend/APIGenerator.java
Normal file
96
src/main/java/com/commafeed/frontend/APIGenerator.java
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package com.commafeed.frontend;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.annotation.processing.AbstractProcessor;
|
||||||
|
import javax.annotation.processing.RoundEnvironment;
|
||||||
|
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||||
|
import javax.annotation.processing.SupportedOptions;
|
||||||
|
import javax.lang.model.element.Element;
|
||||||
|
import javax.lang.model.element.TypeElement;
|
||||||
|
import javax.tools.Diagnostic.Kind;
|
||||||
|
import javax.tools.FileObject;
|
||||||
|
import javax.tools.StandardLocation;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
|
||||||
|
import com.commafeed.frontend.model.Entries;
|
||||||
|
import com.commafeed.frontend.model.request.MarkRequest;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.wordnik.swagger.annotations.Api;
|
||||||
|
import com.wordnik.swagger.core.Documentation;
|
||||||
|
import com.wordnik.swagger.core.DocumentationEndPoint;
|
||||||
|
import com.wordnik.swagger.core.SwaggerSpec;
|
||||||
|
import com.wordnik.swagger.core.util.TypeUtil;
|
||||||
|
import com.wordnik.swagger.jaxrs.HelpApi;
|
||||||
|
import com.wordnik.swagger.jaxrs.JaxrsApiReader;
|
||||||
|
|
||||||
|
@SupportedAnnotationTypes("com.wordnik.swagger.annotations.Api")
|
||||||
|
@SupportedOptions("outputDirectory")
|
||||||
|
public class APIGenerator extends AbstractProcessor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||||
|
try {
|
||||||
|
return processInternal(annotations, roundEnv);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
processingEnv.getMessager().printMessage(Kind.ERROR, e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean processInternal(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws Exception {
|
||||||
|
JaxrsApiReader.setFormatString("");
|
||||||
|
TypeUtil.addAllowablePackage(Entries.class.getPackage().getName());
|
||||||
|
TypeUtil.addAllowablePackage(MarkRequest.class.getPackage().getName());
|
||||||
|
|
||||||
|
String apiVersion = "1.0";
|
||||||
|
String swaggerVersion = SwaggerSpec.version();
|
||||||
|
String basePath = "../rest";
|
||||||
|
|
||||||
|
Documentation doc = new Documentation();
|
||||||
|
for (Element element : roundEnv.getElementsAnnotatedWith(Api.class)) {
|
||||||
|
TypeElement type = (TypeElement) element;
|
||||||
|
String fqn = type.getQualifiedName().toString();
|
||||||
|
Class<?> resource = Class.forName(fqn);
|
||||||
|
|
||||||
|
Api api = resource.getAnnotation(Api.class);
|
||||||
|
String apiPath = api.value();
|
||||||
|
|
||||||
|
Documentation apiDoc = JaxrsApiReader.read(resource, apiVersion, swaggerVersion, basePath, apiPath);
|
||||||
|
apiDoc = new HelpApi(null).filterDocs(apiDoc, null, null, null, null);
|
||||||
|
|
||||||
|
apiDoc.setSwaggerVersion(swaggerVersion);
|
||||||
|
apiDoc.setApiVersion(apiVersion);
|
||||||
|
write(apiDoc, element);
|
||||||
|
|
||||||
|
doc.addApi(new DocumentationEndPoint(api.value(), api.description()));
|
||||||
|
|
||||||
|
}
|
||||||
|
doc.setSwaggerVersion(swaggerVersion);
|
||||||
|
doc.setApiVersion(apiVersion);
|
||||||
|
|
||||||
|
write(doc, null);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(Documentation doc, Element element) throws Exception {
|
||||||
|
String fileName = doc.getResourcePath() == null ? "resources" : doc.getResourcePath();
|
||||||
|
fileName = StringUtils.removeStart(fileName, "/");
|
||||||
|
|
||||||
|
FileObject resource = null;
|
||||||
|
try {
|
||||||
|
resource = processingEnv.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, "", fileName, element);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// already processed
|
||||||
|
}
|
||||||
|
if (resource != null) {
|
||||||
|
FileUtils.writeStringToFile(new File(resource.toUri()), new ObjectMapper().writeValueAsString(doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.commafeed.frontend.rest;
|
|
||||||
|
|
||||||
public class Enums {
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
category, feed, entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ReadType {
|
|
||||||
all, unread;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -32,6 +32,7 @@ import com.commafeed.backend.model.FeedEntryStatus;
|
|||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
import com.commafeed.backend.model.UserRole.Role;
|
||||||
|
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||||
import com.commafeed.backend.services.FeedEntryService;
|
import com.commafeed.backend.services.FeedEntryService;
|
||||||
@@ -47,7 +48,6 @@ import com.commafeed.frontend.model.request.CategoryModificationRequest;
|
|||||||
import com.commafeed.frontend.model.request.CollapseRequest;
|
import com.commafeed.frontend.model.request.CollapseRequest;
|
||||||
import com.commafeed.frontend.model.request.IDRequest;
|
import com.commafeed.frontend.model.request.IDRequest;
|
||||||
import com.commafeed.frontend.model.request.MarkRequest;
|
import com.commafeed.frontend.model.request.MarkRequest;
|
||||||
import com.commafeed.frontend.rest.Enums.ReadType;
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.sun.syndication.feed.synd.SyndEntry;
|
import com.sun.syndication.feed.synd.SyndEntry;
|
||||||
@@ -98,7 +98,7 @@ public class CategoryREST extends AbstractREST {
|
|||||||
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, @ApiParam(
|
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, @ApiParam(
|
||||||
value = "all entries or only unread ones",
|
value = "all entries or only unread ones",
|
||||||
allowableValues = "all,unread",
|
allowableValues = "all,unread",
|
||||||
required = true) @DefaultValue("unread") @QueryParam("readType") ReadType readType, @ApiParam(
|
required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, @ApiParam(
|
||||||
value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||||
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(
|
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(
|
||||||
value = "limit for paging, default 20, maximum 50") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(
|
value = "limit for paging, default 20, maximum 50") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(
|
||||||
@@ -112,7 +112,7 @@ public class CategoryREST extends AbstractREST {
|
|||||||
Entries entries = new Entries();
|
Entries entries = new Entries();
|
||||||
entries.setOffset(offset);
|
entries.setOffset(offset);
|
||||||
entries.setLimit(limit);
|
entries.setLimit(limit);
|
||||||
boolean unreadOnly = readType == ReadType.unread;
|
boolean unreadOnly = readType == ReadingMode.unread;
|
||||||
if (StringUtils.isBlank(id)) {
|
if (StringUtils.isBlank(id)) {
|
||||||
id = ALL;
|
id = ALL;
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ public class CategoryREST extends AbstractREST {
|
|||||||
|
|
||||||
Preconditions.checkNotNull(id);
|
Preconditions.checkNotNull(id);
|
||||||
|
|
||||||
ReadType readType = ReadType.all;
|
ReadingMode readType = ReadingMode.all;
|
||||||
ReadingOrder order = ReadingOrder.desc;
|
ReadingOrder order = ReadingOrder.desc;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
int limit = 20;
|
int limit = 20;
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import com.commafeed.backend.model.FeedCategory;
|
|||||||
import com.commafeed.backend.model.FeedEntryStatus;
|
import com.commafeed.backend.model.FeedEntryStatus;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
import com.commafeed.backend.model.UserRole.Role;
|
||||||
|
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||||
import com.commafeed.backend.services.FeedEntryService;
|
import com.commafeed.backend.services.FeedEntryService;
|
||||||
@@ -68,7 +69,6 @@ import com.commafeed.frontend.model.request.FeedModificationRequest;
|
|||||||
import com.commafeed.frontend.model.request.IDRequest;
|
import com.commafeed.frontend.model.request.IDRequest;
|
||||||
import com.commafeed.frontend.model.request.MarkRequest;
|
import com.commafeed.frontend.model.request.MarkRequest;
|
||||||
import com.commafeed.frontend.model.request.SubscribeRequest;
|
import com.commafeed.frontend.model.request.SubscribeRequest;
|
||||||
import com.commafeed.frontend.rest.Enums.ReadType;
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.sun.syndication.feed.opml.Opml;
|
import com.sun.syndication.feed.opml.Opml;
|
||||||
@@ -135,7 +135,7 @@ public class FeedREST extends AbstractREST {
|
|||||||
public Response getFeedEntries(@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id, @ApiParam(
|
public Response getFeedEntries(@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id, @ApiParam(
|
||||||
value = "all entries or only unread ones",
|
value = "all entries or only unread ones",
|
||||||
allowableValues = "all,unread",
|
allowableValues = "all,unread",
|
||||||
required = true) @DefaultValue("unread") @QueryParam("readType") ReadType readType, @ApiParam(
|
required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, @ApiParam(
|
||||||
value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||||
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(
|
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(
|
||||||
value = "limit for paging, default 20, maximum 50") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(
|
value = "limit for paging, default 20, maximum 50") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(
|
||||||
@@ -152,7 +152,7 @@ public class FeedREST extends AbstractREST {
|
|||||||
entries.setOffset(offset);
|
entries.setOffset(offset);
|
||||||
entries.setLimit(limit);
|
entries.setLimit(limit);
|
||||||
|
|
||||||
boolean unreadOnly = readType == ReadType.unread;
|
boolean unreadOnly = readType == ReadingMode.unread;
|
||||||
|
|
||||||
Date newerThanDate = newerThan == null ? null : new Date(Long.valueOf(newerThan));
|
Date newerThanDate = newerThan == null ? null : new Date(Long.valueOf(newerThan));
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ public class FeedREST extends AbstractREST {
|
|||||||
|
|
||||||
Preconditions.checkNotNull(id);
|
Preconditions.checkNotNull(id);
|
||||||
|
|
||||||
ReadType readType = ReadType.all;
|
ReadingMode readType = ReadingMode.all;
|
||||||
ReadingOrder order = ReadingOrder.desc;
|
ReadingOrder order = ReadingOrder.desc;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
int limit = 20;
|
int limit = 20;
|
||||||
|
|||||||
@@ -32,10 +32,9 @@ public class SwaggerStaticGenerator {
|
|||||||
Api api = resource.getAnnotation(Api.class);
|
Api api = resource.getAnnotation(Api.class);
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
String apiPath = api.value();
|
String apiPath = api.value();
|
||||||
String apiListingPath = api.value();
|
|
||||||
|
|
||||||
Documentation apiDoc = new HelpApi(null).filterDocs(
|
Documentation apiDoc = JaxrsApiReader.read(resource, apiVersion, swaggerVersion, basePath, apiPath);
|
||||||
JaxrsApiReader.read(resource, apiVersion, swaggerVersion, basePath, apiPath), null, null, apiListingPath, apiPath);
|
apiDoc = new HelpApi(null).filterDocs(apiDoc, null, null, null, null);
|
||||||
|
|
||||||
apiDoc.setSwaggerVersion(swaggerVersion);
|
apiDoc.setSwaggerVersion(swaggerVersion);
|
||||||
apiDoc.setApiVersion(apiVersion);
|
apiDoc.setApiVersion(apiVersion);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
link.attr('href', url + '/rest');
|
link.attr('href', url + '/rest');
|
||||||
link.html(link.attr('href'));
|
link.html(link.attr('href'));
|
||||||
|
|
||||||
url = url + '/api/swagger-doc/resources';
|
url = url + '/api/api-docs/resources';
|
||||||
window.swaggerUi = new SwaggerUi({
|
window.swaggerUi = new SwaggerUi({
|
||||||
discoveryUrl:url,
|
discoveryUrl:url,
|
||||||
apiKey:"",
|
apiKey:"",
|
||||||
|
|||||||
@@ -721,7 +721,7 @@ module.controller('FeedListCtrl', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
var callback = function(data) {
|
var callback = function(data) {
|
||||||
if (data.offset == 0) {
|
if (data.offset === 0) {
|
||||||
$scope.entries = [];
|
$scope.entries = [];
|
||||||
}
|
}
|
||||||
for ( var i = 0; i < data.entries.length; i++) {
|
for ( var i = 0; i < data.entries.length; i++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user