Skip to content

Commit

Permalink
feat: sticker console page (#3)
Browse files Browse the repository at this point in the history
#### What type of PR is this?

/kind feature

#### What this PR does / why we need it:

- sticker console page get group and list
- CRUD of sticker group

#### Which issue(s) this PR fixes:

N/A

#### Special notes for your reviewer:

NONE

#### Does this PR introduce a user-facing change?

```release-note
NONE
```
  • Loading branch information
AirboZH authored Oct 31, 2024
1 parent f80ac88 commit 46a2272
Show file tree
Hide file tree
Showing 20 changed files with 2,405 additions and 251 deletions.
123 changes: 104 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,117 @@
# plugin-sticker
# Plugin-Sticker Halo 表情包插件

## 开发记录
## 简介

### 模型设计
这是为 Halo 博客系统开发的表情包管理插件。允许用户上传、管理和使用自定义表情包,增强博客的互动性和个性化。

#### Sticker
## 功能特点

- 表情包分组管理
- 表情包上传和删除
- 表情包搜索功能
- 权限控制
- 与 Halo 富文本编辑器集成

## 安装

### 接口设计
1. 下载最新的发布版本。
2. 在 Halo 管理界面中,导航至 `插件` -> `上传插件`
3. 选择下载的插件文件并上传。
4. 启用插件。

#### GET `/sticker/{stickerid}`
获取具体表情包 (所有人)
## 使用方法

#### POST `/sticker/{username}/upload`
用户表情包上传 (本人)
#### DELETE `/sticker/{stickerid}`
删除用户表情包 (本人 | 管理员)
### 管理表情包

#### GET `/stickers/{username}/`
获取用户表情包列表 (本人 | 管理员)
1. 在 Halo 管理界面中,找到 "表情包" 菜单项。
2. 您可以在这里创建新的表情包分组,或管理现有的分组。
3. 点击分组后,可以上传新的表情包或管理现有的表情包。

#### GET `/stickergroup`
获取分组表情包列表(管理员)
#### POST `/stickergroup`
创建分组(管理员)
#### DELETE `/stickergroup/{groupname}`
删除分组(管理员)
### 在编辑器中使用表情包

1. 在使用富文本编辑器时,找到表情包按钮。
2. 点击后会打开表情包选择器。
3. 选择想要插入的表情包,它会自动插入到当前光标位置。

## 开发

### 项目结构
```
.
├── src
│ ├── main
│ │ ├── java/run/halo/sticker
│ │ │ ├── endpoint
│ │ │ │ ├── StickerEndpoint.java
│ │ │ │ └── StickerGroupEndpoint.java
│ │ │ ├── infra
│ │ │ │ └── StickerSetting.java
│ │ │ ├── model
│ │ │ │ ├── Sticker.java
│ │ │ │ └── StickerGroup.java
│ │ │ ├── pojo
│ │ │ │ ├── enums
│ │ │ │ │ └── StickerSorter.java
│ │ │ │ └── query
│ │ │ │ └── StickerQuery.java
│ │ │ ├── reconciler
│ │ │ │ └── StickerReconciler.java
│ │ │ ├── service
│ │ │ │ ├── impl
│ │ │ │ │ └── StickerServiceImpl.java
│ │ │ │ └── StickerService.java
│ │ │ └── StickerPlugin.java
│ │ └── resources
│ │ ├── extensions
│ │ │ ├── attachment-local-policy.yaml
│ │ │ ├── role-template-sticker.yaml
│ │ │ └── settings.yaml
│ │ ├── logo.png
│ │ └── plugin.yaml
├── ui
│ ├── src
│ │ ├── components
│ │ │ ├── GroupEditingModal.vue
│ │ │ ├── LazyImage.vue
│ │ │ ├── StickerGroupList.vue
│ │ │ ├── StickerGroupListUser.vue
│ │ │ └── StickerPicker.vue
│ │ ├── editor
│ │ │ └── index.ts
│ │ ├── views
│ │ │ ├── StickerManage.vue
│ │ │ └── StickerManageUser.vue
│ │ └── index.ts
│ ├── package.json
│ └── vite.config.ts
├── build.gradle
├── gradle.properties
├── settings.gradle
└── README.md
```

### 构建

```bash
./gradlew build --stacktrace
```

### 打包

```bash
./gradlew jar
```

## API 参考

- `GET /apis/sticker.api.halo.run/v1alpha1/stickers`: 获取表情包列表
- `POST /apis/sticker.api.halo.run/v1alpha1/stickers/-/upload`: 上传新表情包
- `DELETE /apis/sticker.api.halo.run/v1alpha1/stickers/{name}`: 删除指定表情包
- `GET /apis/sticker.api.halo.run/v1alpha1/stickerGroups`: 获取表情包分组列表
- `POST /apis/sticker.api.halo.run/v1alpha1/stickerGroups`: 创建新的表情包分组
- `PUT /apis/sticker.api.halo.run/v1alpha1/stickerGroups/{name}`: 更新指定表情包分组
- `DELETE /apis/sticker.api.halo.run/v1alpha1/stickerGroups/{name}`: 删除指定表情包分组

## 许可证

本项目采用 [GPL-3.0 license](https://github.com/halo-sigs/plugin-sticker/blob/main/LICENSE)
13 changes: 12 additions & 1 deletion src/main/java/run/halo/sticker/StickerPlugin.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package run.halo.sticker;

import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;

import org.springframework.stereotype.Component;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.index.IndexSpec;
import run.halo.app.plugin.BasePlugin;
import run.halo.app.plugin.PluginContext;
import run.halo.sticker.model.Sticker;
Expand All @@ -19,7 +22,15 @@ public StickerPlugin(PluginContext pluginContext, SchemeManager schemeManager) {

@Override
public void start() {
schemeManager.register(Sticker.class);
schemeManager.register(Sticker.class,indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.groupName")
.setIndexFunc(simpleAttribute(Sticker.class, moment -> {
var tags = moment.getSpec().getGroupName();
return tags == null ? "" : tags;
}))
);
});
schemeManager.register(StickerGroup.class);
}

Expand Down
45 changes: 40 additions & 5 deletions src/main/java/run/halo/sticker/endpoint/StickerEndpoint.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package run.halo.sticker.endpoint;

import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

import com.google.common.io.Files;
import java.security.Principal;
Expand Down Expand Up @@ -33,6 +34,7 @@
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.core.extension.service.AttachmentService;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.plugin.ReactiveSettingFetcher;
Expand All @@ -58,16 +60,43 @@ public class StickerEndpoint implements CustomEndpoint {

@Override
public RouterFunction<ServerResponse> endpoint() {
final var tag = "sticker.api.halo.run/v1alpha1/Sticker";
return route()
.GET("stickers", this::listStickersByGroup)
.POST("sticker/-/upload", contentType(MediaType.MULTIPART_FORM_DATA),
this::uploadUserSticker)
.GET("stickers", this::listStickersByGroup,
builder -> {
builder.operationId("ListStickers")
.description("List stickers by group.")
.tag(tag)
.response(responseBuilder().implementation(
ListResult.generateGenericClass(Sticker.class))
);
// 如果有查询参数,可以在这里添加
// StickerQuery.buildParameters(builder);
}
)
.POST("stickers/-/upload", contentType(MediaType.MULTIPART_FORM_DATA),
this::uploadUserSticker,
builder -> {
builder.operationId("UploadSticker")
.description("Upload a user sticker.")
.tag(tag)
.response(responseBuilder().implementation(Sticker.class));
}
)
.DELETE("stickers/{name}", this::deleteStickers,
builder -> {
builder.operationId("DeleteSticker")
.description("Delete a sticker.")
.tag(tag)
.response(responseBuilder().implementation(Void.class));
}
)
.build();
}

@Override
public GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("console.api.sticker.halo.run/v1alpha1");
return GroupVersion.parseAPIVersion("sticker.api.halo.run/v1alpha1");
}

private Mono<ServerResponse> listStickersByGroup(ServerRequest request) {
Expand All @@ -94,6 +123,11 @@ private Mono<ServerResponse> uploadUserSticker(ServerRequest request) {
.flatMap(sticker -> ServerResponse.ok().bodyValue(sticker));
}

private Mono<ServerResponse> deleteStickers(ServerRequest request) {
log.info("Deleting sticker");
return ServerResponse.ok().build();
}

private Mono<Sticker> saveSticker(UploadAttachmentDto dto) {
var sticker = new Sticker();
var metadata = new Metadata();
Expand All @@ -108,6 +142,7 @@ private Mono<Sticker> saveSticker(UploadAttachmentDto dto) {
}

private Mono<StickerGroup> getOrCreateStickerGroup(String groupName) {
//todo find the default group
String finalGroupName = SELF_USER.equals(groupName) ? UUID.randomUUID().toString() : groupName;
return client.fetch(StickerGroup.class, finalGroupName)
.switchIfEmpty(getUserName().flatMap(userName -> createSelfStickerGroup(finalGroupName, userName)));
Expand Down
129 changes: 129 additions & 0 deletions src/main/java/run/halo/sticker/endpoint/StickerGroupEndpoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package run.halo.sticker.endpoint;

import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;

import java.security.Principal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.sticker.model.StickerGroup;

@Slf4j
@Component
@RequiredArgsConstructor
public class StickerGroupEndpoint implements CustomEndpoint {

private final ReactiveExtensionClient client;

@Override
public RouterFunction<ServerResponse> endpoint() {
final var tag = "sticker.api.halo.run/v1alpha1/StickerGroup";
return route()
.GET("stickerGroups", this::listStickerGroups,
builder -> builder.operationId("ListStickerGroups")
.description("List sticker groups.")
.tag(tag)
.response(responseBuilder().implementation(
ListResult.generateGenericClass(StickerGroup.class))))
.POST("stickerGroups", this::createStickerGroup,
builder -> builder.operationId("CreateStickerGroup")
.description("Create a sticker group.")
.tag(tag)
.response(responseBuilder().implementation(StickerGroup.class)))
.PUT("stickerGroups/{name}", this::updateStickerGroup,
builder -> builder.operationId("UpdateStickerGroup")
.description("Update a sticker group.")
.tag(tag)
.response(responseBuilder().implementation(StickerGroup.class)))
.DELETE("stickerGroups/{name}", this::deleteStickerGroup,
builder -> builder.operationId("DeleteStickerGroup")
.description("Delete a sticker group.")
.tag(tag)
.response(responseBuilder().implementation(Void.class)))
.build();
}

@Override
public GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("sticker.api.halo.run/v1alpha1");
}

private Mono<ServerResponse> listStickerGroups(ServerRequest request) {
return getUserName()
.flatMap(username -> client.list(StickerGroup.class,
stickerGroup -> username.equals(stickerGroup.getSpec().getOwner())
|| stickerGroup.getSpec().getIsPublic(),
null)
.collectList()
.flatMap(list -> {
if (list.isEmpty()) {
return createDefaultStickerGroup(username)
.map(defaultGroup -> {
list.add(defaultGroup);
return list;
});
}
return Mono.just(list);
}))
.flatMap(list -> ServerResponse.ok().bodyValue(list));
}

private Mono<StickerGroup> createDefaultStickerGroup(String username) {
StickerGroup defaultGroup = new StickerGroup();
defaultGroup.setMetadata(new Metadata());
defaultGroup.getMetadata().setName(username + "-stickers");
defaultGroup.setSpec(new StickerGroup.StickerGroupSpec());
defaultGroup.getSpec().setDisplayName("我的表情");
defaultGroup.getSpec().setIsPublic(false);
defaultGroup.getSpec().setIsDefault(true);
defaultGroup.getSpec().setOwner(username);

return client.create(defaultGroup);
}

private Mono<ServerResponse> createStickerGroup(ServerRequest request) {
return getUserName().flatMap(username -> request.bodyToMono(StickerGroup.class)
.doOnNext(stickerGroup -> stickerGroup.getSpec().setOwner(username))
.flatMap(client::create)).flatMap(created -> ServerResponse.ok().bodyValue(created));
}

private Mono<ServerResponse> updateStickerGroup(ServerRequest request) {
String name = request.pathVariable("name");
return getUserName().flatMap(username -> client.get(StickerGroup.class, name)
.filter(group -> username.equals(group.getSpec().getOwner()))
.switchIfEmpty(Mono.error(new IllegalAccessException("您没有权限更新这个群组")))
.flatMap(
existingGroup -> request.bodyToMono(StickerGroup.class).doOnNext(updatedGroup -> {
updatedGroup.getMetadata().setName(name);
updatedGroup.getSpec().setOwner(username);
}).flatMap(client::update)))
.flatMap(updated -> ServerResponse.ok().bodyValue(updated))
.onErrorResume(IllegalAccessException.class, e -> ServerResponse.notFound().build());
}

private Mono<ServerResponse> deleteStickerGroup(ServerRequest request) {
String name = request.pathVariable("name");
return getUserName().flatMap(username -> client.get(StickerGroup.class, name)
.filter(group -> username.equals(group.getSpec().getOwner()))
.switchIfEmpty(Mono.error(new IllegalAccessException("您没有权限删除这个群组")))
.flatMap(client::delete)).then(ServerResponse.noContent().build())
.onErrorResume(IllegalAccessException.class, e -> ServerResponse.notFound().build());
}

private Mono<String> getUserName() {
return ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication)
.map(Principal::getName);
}
}
Loading

0 comments on commit 46a2272

Please sign in to comment.