support add media thumb attachment
This commit is contained in:
@ -0,0 +1,39 @@
|
||||
package com.imyeyu.api.modules.common.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2025-10-20 15:04
|
||||
*/
|
||||
public class MediaAttach {
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2025-09-28 02:01
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
SOURCE,
|
||||
|
||||
THUMB
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-10-20 15:04
|
||||
*/
|
||||
@Data
|
||||
public static class ExtData {
|
||||
|
||||
private Long sourceId;
|
||||
|
||||
private String sourceMongoId;
|
||||
|
||||
private boolean isImage;
|
||||
|
||||
private boolean isVideo;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package com.imyeyu.api.modules.common.entity;
|
||||
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.api.bean.MultilingualHandler;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.spring.entity.Entity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@ -44,24 +44,36 @@ public class Attachment extends Entity implements MultilingualHandler {
|
||||
/** 镜像 */
|
||||
MIRROR,
|
||||
|
||||
JOURNAL,
|
||||
|
||||
JOURNAL_TRAVEL,
|
||||
|
||||
JOURNAL_MOMENT,
|
||||
|
||||
/** 系统 */
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
private BizType bizType;
|
||||
protected BizType bizType;
|
||||
|
||||
private Long bizId;
|
||||
protected Long bizId;
|
||||
|
||||
private String attachType;
|
||||
protected String attachType;
|
||||
|
||||
private String mongoId;
|
||||
protected String mongoId;
|
||||
|
||||
@MultilingualField
|
||||
private String title;
|
||||
protected String title;
|
||||
|
||||
private String name;
|
||||
protected String name;
|
||||
|
||||
private Long size;
|
||||
protected Long size;
|
||||
|
||||
protected String md5;
|
||||
|
||||
protected String ext;
|
||||
|
||||
protected Long destroyAt;
|
||||
|
||||
public void setAttachTypeValue(Enum<?> attachType) {
|
||||
this.attachType = attachType.toString();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.imyeyu.api.modules.common.mapper;
|
||||
|
||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@ -24,8 +25,9 @@ public interface AttachmentMapper extends BaseMapper<Attachment, Long> {
|
||||
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND attach_type = #{attachType} " + VALID + LIMIT_1)
|
||||
Attachment selectByAttachType(Attachment.BizType bizType, long bizId, Enum<?> attachType);
|
||||
|
||||
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND " + VALID + PAGE)
|
||||
List<Attachment> listByBizId(Attachment.BizType bizType, long bizId, long offset, int limit);
|
||||
List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, Page page);
|
||||
|
||||
List<Attachment> listByAttachType(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
|
||||
long countByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes);
|
||||
|
||||
List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, List<String> md5s);
|
||||
}
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
package com.imyeyu.api.modules.common.service;
|
||||
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
|
||||
import com.imyeyu.api.modules.common.vo.attachment.AttachmentView;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import com.imyeyu.spring.service.DeletableService;
|
||||
import com.imyeyu.spring.service.DestroyableService;
|
||||
import com.imyeyu.spring.service.GettableService;
|
||||
import com.imyeyu.spring.service.PageableService;
|
||||
import com.imyeyu.spring.service.UpdatableService;
|
||||
import com.mongodb.client.gridfs.model.GridFSFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
@ -20,7 +24,7 @@ import java.util.List;
|
||||
* @author 夜雨
|
||||
* @since 2023-08-15 10:21
|
||||
*/
|
||||
public interface AttachmentService extends GettableService<Attachment, Long>, DeletableService<Long>, DestroyableService<Long> {
|
||||
public interface AttachmentService extends GettableService<Attachment, Long>, PageableService<Attachment>, UpdatableService<Attachment>, DeletableService<Long>, DestroyableService<Long> {
|
||||
|
||||
/**
|
||||
*
|
||||
@ -29,6 +33,16 @@ public interface AttachmentService extends GettableService<Attachment, Long>, De
|
||||
*/
|
||||
void create(AttachmentRequest request);
|
||||
|
||||
/**
|
||||
* 创建媒体附件,同步创建缩略图
|
||||
*
|
||||
* @param request 附件请求
|
||||
* @return 缩略图附件
|
||||
*/
|
||||
Attachment createMedia(AttachmentRequest request) throws TimiException;
|
||||
|
||||
void deleteMedia(Long thumbId) throws TimiException;
|
||||
|
||||
Attachment getByBizId(Attachment.BizType bizType, long bizId);
|
||||
|
||||
Attachment getByAttachType(Attachment.BizType bizType, long bizId, Enum<?> attachType);
|
||||
@ -47,10 +61,25 @@ public interface AttachmentService extends GettableService<Attachment, Long>, De
|
||||
* 根据业务获取所有附件
|
||||
*
|
||||
* @param bizType 业务类型
|
||||
* @param bizId 业务 ID
|
||||
* @param attachTypes
|
||||
* @param bizId 业务 ID,可为 null
|
||||
* @param attachTypes 附件类型,可为 null
|
||||
* @return 所有附件
|
||||
* @throws TimiException 服务异常
|
||||
*/
|
||||
List<Attachment> listByBizId(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
|
||||
List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, Enum<?> ...attachTypes);
|
||||
|
||||
/**
|
||||
* 根据业务获取所有附件
|
||||
*
|
||||
* @param bizType 业务类型
|
||||
* @param bizId 业务 ID,可为 null
|
||||
* @param attachTypes 附件类型,可为 null
|
||||
* @return 附件数量
|
||||
* @throws TimiException 服务异常
|
||||
*/
|
||||
long countByBizId(Attachment.BizType bizType, Long bizId, Enum<?> ...attachTypes);
|
||||
|
||||
List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, List<String> md5s) throws TimiException;
|
||||
|
||||
PageResult<Attachment> pageByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, Page page) throws TimiException;
|
||||
}
|
||||
|
||||
@ -1,15 +1,22 @@
|
||||
package com.imyeyu.api.modules.common.service.implement;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||
import com.imyeyu.api.modules.common.bean.MediaAttach;
|
||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||
import com.imyeyu.api.modules.common.mapper.AttachmentMapper;
|
||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||
import com.imyeyu.api.modules.common.service.SettingService;
|
||||
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
|
||||
import com.imyeyu.api.modules.common.vo.attachment.AttachmentView;
|
||||
import com.imyeyu.api.util.JavaCV;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||
import com.imyeyu.api.modules.common.mapper.AttachmentMapper;
|
||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
|
||||
import com.imyeyu.api.modules.common.vo.attachment.AttachmentView;
|
||||
import com.imyeyu.network.Network;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import com.imyeyu.spring.service.AbstractEntityService;
|
||||
import com.imyeyu.utils.Time;
|
||||
@ -17,6 +24,8 @@ import com.mongodb.client.gridfs.GridFSBucket;
|
||||
import com.mongodb.client.gridfs.model.GridFSFile;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.coobird.thumbnailator.Thumbnails;
|
||||
import org.apache.tika.Tika;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
@ -24,9 +33,14 @@ import org.springframework.data.mongodb.gridfs.GridFsTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
@ -37,8 +51,11 @@ import java.util.List;
|
||||
@RequiredArgsConstructor
|
||||
public class AttachmentServiceImplement extends AbstractEntityService<Attachment, Long> implements AttachmentService {
|
||||
|
||||
private final SettingService settingService;
|
||||
|
||||
private final AttachmentMapper mapper;
|
||||
|
||||
private final Gson gson;
|
||||
private final GridFSBucket gridFSBucket;
|
||||
private final GridFsTemplate gridFsTemplate;
|
||||
|
||||
@ -52,11 +69,11 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
||||
public void destroy(Long id) {
|
||||
try {
|
||||
Attachment attachment = get(id);
|
||||
if (!attachment.isDeleted()) {
|
||||
delete(id);
|
||||
}
|
||||
mapper.destroy(attachment.getId());
|
||||
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attachment.getMongoId())));
|
||||
|
||||
attachment.setDeletedAt(Time.now());
|
||||
attachment.setDestroyAt(Time.now());
|
||||
mapper.update(attachment);
|
||||
} catch (Exception e) {
|
||||
log.error("delete mongo file error", e);
|
||||
throw new TimiException(TimiCode.ERROR).msgKey("TODO delete mongo file error");
|
||||
@ -81,12 +98,12 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
||||
}
|
||||
mongoName.append(request.getName());
|
||||
|
||||
mongoId = gridFsTemplate.store(is, mongoName.toString()).toString();
|
||||
Attachment attachment = new Attachment();
|
||||
BeanUtils.copyProperties(request, attachment);
|
||||
attachment.setMongoId(mongoId);
|
||||
attachment.setCreatedAt(Time.now());
|
||||
mapper.insert(attachment);
|
||||
mongoId = gridFsTemplate.store(request.getInputStream(), mongoName.toString()).toString();
|
||||
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(mongoId)));
|
||||
request.setMongoId(mongoId);
|
||||
request.setSize(gridFSFile.getLength());
|
||||
request.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
|
||||
mapper.insert(request);
|
||||
} catch (Exception e) {
|
||||
if (mongoId != null) {
|
||||
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(mongoId)));
|
||||
@ -96,6 +113,74 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public Attachment createMedia(AttachmentRequest request) throws TimiException {
|
||||
TimiException.required(request.getName(), "not found name");
|
||||
TimiException.required(request.getBizType(), "not found bizType");
|
||||
TimiException.required(request.getInputStream(), "not found inputStream");
|
||||
try {
|
||||
// 储存源文件
|
||||
request.setAttachTypeValue(MediaAttach.Type.SOURCE);
|
||||
create(request);
|
||||
|
||||
// 生成缩略图
|
||||
InputStream mimeStream = getInputStreamByMongoId(request.getMongoId());
|
||||
String mimeType = new Tika().detect(mimeStream);
|
||||
boolean isImage = false;
|
||||
|
||||
InputStream sourceStream = getInputStreamByMongoId(request.getMongoId());
|
||||
ByteArrayOutputStream thumbStream = new ByteArrayOutputStream();
|
||||
switch (mimeType) {
|
||||
case "image/png", "image/bmp", "image/jpeg" -> {
|
||||
isImage = true;
|
||||
Thumbnails.of(sourceStream).width(256).keepAspectRatio(true).toOutputStream(thumbStream);
|
||||
}
|
||||
case "video/mp4", "video/quicktime" -> {
|
||||
log.info("capturing thumbnail: {}", request.getName());
|
||||
long start = Time.now();
|
||||
File tempFile = IO.file("temp/%s_%s".formatted(UUID.randomUUID().toString(), request.getName()));
|
||||
try {
|
||||
IO.toFile(tempFile, sourceStream);
|
||||
ByteArrayOutputStream baos = JavaCV.captureThumbnail(IO.getInputStream(tempFile), 2);
|
||||
Thumbnails.of(IO.toInputStream(baos)).width(256).keepAspectRatio(true).toOutputStream(thumbStream);
|
||||
log.info("captured thumbnail: {} at {} ms", request.getName(), Time.now() - start);
|
||||
} finally {
|
||||
IO.destroy(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
MediaAttach.ExtData extData = new MediaAttach.ExtData();
|
||||
extData.setImage(isImage);
|
||||
extData.setVideo(!isImage);
|
||||
extData.setSourceId(request.getId());
|
||||
extData.setSourceMongoId(request.getMongoId());
|
||||
|
||||
AttachmentRequest thumbAttach = new AttachmentRequest();
|
||||
thumbAttach.setName(Network.simpleURIFileName(request.getName()) + ".png");
|
||||
thumbAttach.setBizType(request.getBizType());
|
||||
thumbAttach.setBizId(request.getBizId());
|
||||
thumbAttach.setAttachTypeValue(MediaAttach.Type.THUMB);
|
||||
thumbAttach.setExt(gson.toJson(extData));
|
||||
thumbAttach.setInputStream(new ByteArrayInputStream(thumbStream.toByteArray()));
|
||||
create(thumbAttach);
|
||||
|
||||
return get(thumbAttach.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("create media attachment error", e);
|
||||
throw new TimiException(TimiCode.ERROR).msgKey("TODO create media attachment error");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void deleteMedia(Long thumbId) throws TimiException {
|
||||
Attachment attachment = get(thumbId);
|
||||
delete(attachment.getId());
|
||||
MediaAttach.ExtData data = gson.fromJson(attachment.getExt(), MediaAttach.ExtData.class);
|
||||
delete(data.getSourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attachment getByBizId(Attachment.BizType bizType, long bizId) {
|
||||
return mapper.selectByBizId(bizType, bizId);
|
||||
@ -144,7 +229,25 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Attachment> listByBizId(Attachment.BizType bizType, long bizId, Enum<?>... attachTypes) {
|
||||
return mapper.listByAttachType(bizType, bizId, attachTypes);
|
||||
public List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, Enum<?>... attachTypes) {
|
||||
return mapper.listByBizId(bizType, bizId, Arrays.asList(attachTypes), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countByBizId(Attachment.BizType bizType, Long bizId, Enum<?>... attachTypes) {
|
||||
return mapper.countByBizId(bizType, bizId, Arrays.asList(attachTypes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, List<String> md5s) throws TimiException {
|
||||
return mapper.listByMd5s(bizType, bizId, attachTypeList, md5s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<Attachment> pageByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, Page page) throws TimiException {
|
||||
PageResult<Attachment> result = new PageResult<>();
|
||||
result.setList(mapper.listByBizId(bizType, bizId, attachTypeList, page));
|
||||
result.setTotal(mapper.countByBizId(bizType, bizId, attachTypeList));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
42
src/main/java/com/imyeyu/api/util/JavaCV.java
Normal file
42
src/main/java/com/imyeyu/api/util/JavaCV.java
Normal file
@ -0,0 +1,42 @@
|
||||
package com.imyeyu.api.util;
|
||||
|
||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.bytedeco.javacv.Java2DFrameConverter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-10-23 17:05
|
||||
*/
|
||||
public class JavaCV {
|
||||
|
||||
public static ByteArrayOutputStream captureThumbnail(InputStream stream, double targetSeconds) throws Exception {
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(stream)) {
|
||||
grabber.start();
|
||||
long targetMillis = (long) (targetSeconds * 1000);
|
||||
grabber.setTimestamp(targetMillis);
|
||||
Frame frame;
|
||||
while ((frame = grabber.grabImage()) != null) {
|
||||
if (grabber.getTimestamp() >= targetMillis) {
|
||||
Java2DFrameConverter converter = new Java2DFrameConverter();
|
||||
try (converter) {
|
||||
BufferedImage bi = converter.getBufferedImage(frame);
|
||||
if (bi != null) {
|
||||
ImageIO.write(bi, "png", outStream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return outStream;
|
||||
}
|
||||
}
|
||||
@ -2,15 +2,66 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="com.imyeyu.api.modules.common.mapper.AttachmentMapper">
|
||||
<sql id="table">attachment</sql>
|
||||
<select id="listByAttachType" resultType="com.imyeyu.api.modules.common.entity.Attachment">
|
||||
<select id="listByBizId" resultType="com.imyeyu.api.modules.common.entity.Attachment">
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
<include refid="table" />
|
||||
attachment
|
||||
WHERE
|
||||
biz_type = #{bizType}
|
||||
<if test="bizId != null">
|
||||
AND biz_id = #{bizId}
|
||||
<if test="attachTypes != null and 0 < attachTypes.length">
|
||||
</if>
|
||||
<if test="attachTypes != null and 0 < attachTypes.size()">
|
||||
AND attach_type IN
|
||||
<foreach collection="attachTypes" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
</if>
|
||||
AND deleted_at IS NULL
|
||||
AND destroy_at IS NULL
|
||||
<if test="page != null">
|
||||
<if test="page.orderMap != null and !page.orderMap.isEmpty()">
|
||||
ORDER BY
|
||||
<foreach collection="page.orderMap" index="key" item="value" separator=",">
|
||||
${key} ${value}
|
||||
</foreach>
|
||||
</if>
|
||||
LIMIT
|
||||
#{page.offset},
|
||||
#{page.limit}
|
||||
</if>
|
||||
</select>
|
||||
<select id="countByBizId" resultType="long">
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
attachment
|
||||
WHERE
|
||||
biz_type = #{bizType}
|
||||
<if test="bizId != null">
|
||||
AND biz_id = #{bizId}
|
||||
</if>
|
||||
<if test="attachTypes != null and 0 < attachTypes.size()">
|
||||
AND attach_type IN
|
||||
<foreach collection="attachTypes" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
</if>
|
||||
AND deleted_at IS NULL
|
||||
AND destroy_at IS NULL
|
||||
</select>
|
||||
<select id="listByMd5s" resultType="com.imyeyu.api.modules.common.entity.Attachment">
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
attachment
|
||||
WHERE
|
||||
biz_type = #{bizType}
|
||||
<if test="bizId != null">
|
||||
AND biz_id = #{bizId}
|
||||
</if>
|
||||
<if test="attachTypes != null and 0 < attachTypes.size()">
|
||||
AND attach_type IN
|
||||
<foreach collection="attachTypes" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
|
||||
Reference in New Issue
Block a user