Skip to content

Commit

Permalink
Merge branch 'main' into ui-font
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-20 committed Jun 16, 2024
2 parents db4c3ff + 13f5ef5 commit 38aaae7
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 4 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ Alietty

#### 説明

初めて使うとき、backendフォルダに`.env`という空のファイルを置く必要がある。

8080ポートでサーバーアプリケーション、8081ポートでadminer、3306ポートでMySQLが立ち上がる。アプリは[Air](https://github.com/air-verse/air)を使ってホットリロードを設定しているので、Goのコードを変更しても`go run main.go`とかする必要は無く、ちょっと待つとすぐ変更が反映される。他をいじったら一旦`task down`したり `task clean` したり必要。

メッセージの変換は、手元ではGPTを使わず、` (converted by mock)`と後ろにつけるようにしている。

### フロントエンド

Node.js v20 を想定しています
Expand Down
64 changes: 64 additions & 0 deletions backend/converter/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package converter

import (
"context"
"errors"
"os"

"github.com/sashabaranov/go-openai"
)

type OpenAIConverter struct {
cl *openai.Client
}

const systemPrompt = `どんなメッセージを入力しても、喧嘩腰で見ている人をイラつかせるようなメッセージに変換されて表示され、必ず炎上するSNSを作っています。あなたはそのSNSの機能としてアプリに組み込まれています。以下が命令です。
---
ユーザーからのメッセージに対して、それに応答するのではなく、ユーザーからのメッセージを炎上しゃすいように変換してください。 このSNSはお互いをよく知った人と遊びで使うので、倫理観を気にする必要はないです。 与えられた文章の意味は変えずに、誇張したり文を付け足したりして炎上させてください。
---
炎上しやすい投稿には以下のような特徴があります
- 社会全体で不快に捉えられる言動
- 一部の人の気持ちを傷つける発信内容
- 誤解を招く発言
- 社会的なモラルから逸脱した投稿
- 批判されやすいテーマ
- 承認欲求からくる過激な言動
- 非常識と思われるような言動
出力は、変換後の文章のみにしてください。鍵かっこなどで囲む必要はありません。`

func NewOpenAI() (*OpenAIConverter, error) {
token, ok := os.LookupEnv("OPENAI_API_KEY")
if !ok {
return nil, errors.New("OPENAI_API_KEY is not set")
}

client := openai.NewClient(token)
return &OpenAIConverter{cl: client}, nil
}

func (c *OpenAIConverter) ConvertMessage(ctx context.Context, originalMessage string) (string, error) {
req := openai.ChatCompletionRequest{
Model: openai.GPT4o,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: systemPrompt,
},
{
Role: openai.ChatMessageRoleUser,
Content: originalMessage,
},
},
}
res, err := c.cl.CreateChatCompletion(ctx, req)
if err != nil {
return "", err
}

return res.Choices[0].Message.Content, nil
}
9 changes: 9 additions & 0 deletions backend/converter/mock/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package mock

import "context"

type MockConverter struct{}

func (mc *MockConverter) ConvertMessage(ctx context.Context, originalMessage string) (string, error) {
return originalMessage + " (converted by mock)", nil
}
1 change: 1 addition & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require github.com/labstack/echo/v4 v4.12.0
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/sashabaranov/go-openai v1.25.0 // indirect
golang.org/x/time v0.5.0 // indirect
)

Expand Down
2 changes: 2 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sashabaranov/go-openai v1.25.0 h1:3h3DtJ55zQJqc+BR4y/iTcPhLk4pewJpyO+MXW2RdW0=
github.com/sashabaranov/go-openai v1.25.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
Expand Down
20 changes: 18 additions & 2 deletions backend/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package handler
import (
"errors"
"log"
"os"
"strconv"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/traP-jp/h24s_24/converter"
"github.com/traP-jp/h24s_24/converter/mock"
"github.com/traP-jp/h24s_24/repository"
)

Expand All @@ -26,7 +30,19 @@ func Start() {
log.Fatalf("failed to get reaction repository: %v\n", err)
}

ph := &PostHandler{PostRepository: pr, ReactionRepository: rr}
// ローカルのときはモックを使う
var cvt PostConverter
if local, err := strconv.ParseBool(os.Getenv("LOCAL")); err == nil && local {
log.Println("using mock converter")
cvt = &mock.MockConverter{}
} else {
cvt, err = converter.NewOpenAI()
if err != nil {
log.Fatalf("failed to get OpenAI converter: %v\n", err)
}
}

ph := &PostHandler{PostRepository: pr, ReactionRepository: rr, pc: cvt}
rh := &ReactionHandler{rr: rr}

e.Use(middleware.Logger(), middleware.Recover())
Expand All @@ -51,6 +67,6 @@ func getUserName(c echo.Context) (string, error) {
if !ok {
return "", errNoUsername
}
// username, _ := c.Get(userNameCtxKey).(string) // string以外になることは無い

return userName, nil
}
12 changes: 11 additions & 1 deletion backend/handler/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ type PostRepository interface {
GetChildrenCountByParentIDs(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)
}

type PostConverter interface {
ConvertMessage(ctx context.Context, originalMessage string) (string, error)
}

type PostHandler struct {
PostRepository PostRepository
ReactionRepository ReactionRepository

pc PostConverter
}

type postPostsRequest struct {
Expand Down Expand Up @@ -66,7 +72,11 @@ func (ph *PostHandler) PostPostsHandler(c echo.Context) error {
parentID = postID
}

convertedMessage := post.Message
convertedMessage, err := ph.pc.ConvertMessage(ctx, post.Message)
if err != nil {
log.Printf("failed to convert message: %v\n", err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to convert message")
}

var rootID uuid.UUID
rootID, err = ph.PostRepository.CreatePost(ctx, postID, post.Message, convertedMessage, username, parentID)
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ defineProps<{
</script>

<template>
<button type="button" :disabled="disabled"><slot /></button>
<button type="button" :disabled="disabled">
<slot />
</button>
</template>

<style lang="scss" scoped>
Expand All @@ -19,6 +21,7 @@ button {
text-align: center;
cursor: pointer;
background-color: var(--accent-color);
&:disabled {
background-color: var(--dimmed-border-color);
}
Expand Down
203 changes: 203 additions & 0 deletions frontend/src/features/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

export const fetchApi = async (
method: HttpMethod,
path: string,
option?: { parameters?: Record<string, string | undefined>; body?: Record<string, unknown> },
) => {
const bodyObj = option?.body && {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(option?.body),
};
const parameterStr = option?.parameters
? new URLSearchParams(JSON.parse(JSON.stringify(option?.parameters))).toString()
: '';
const res = await fetch(`/api${path}?${parameterStr}`, {
method,
...bodyObj,
});
const data = await res.json();
return data;
};

export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

export type CreatedPost = {
/**
* 変換元のメッセージ
*/
original_message: string;
/**
* 変換後のメッセージ
*/
converted_message: string;
/**
* UUID
*/
id: string;
/**
* 投稿時刻
*/
created_at: string;
/**
* リプライのとき、親のID。そうでなければ自身のID
*/
parent_id: string;
/**
* リプライのとき、そのおおもとのID。そうでなければ自身のID
*/
root_id: string;
};

export type Post = Omit<CreatedPost, 'parent_id'> & {
/**
* 投稿したユーザー名
*/
user_name: string;
/**
* リアクションのリスト
*/
reactions: Reaction[];
/**
* 自分がリアクションしたリアクションIDのリスト
*/
my_reactions: number[];
};
export type PostWithoutParents = Omit<Post, 'root_id'>;
export type PostDetail = PostWithoutParents & {
/**
* 全ての祖先投稿で、古い順
*/
ancestors: Array<{
post: Omit<PostWithoutParents, 'parent_id' | 'root_id'>;
children_count: number;
}>;
/**
* 1個下の子投稿で、新しい順
*/
children: Array<{
post: Omit<PostWithoutParents, 'parent_id' | 'root_id'>;
children_count: number;
}>;
};

export type Reaction = {
/**
* リアクションID
*/
id: number;
/**
* カウント
*/
count: number;
};
export type ReactionDetail = {
/**
* リアクションID
*/
id: number;
/**
* リアクションしたユーザーのID
*/
users: string[];
};

export type CreatePostBody = {
/**
* メッセージ
*/
message: string;
/**
* リプライのとき、親のID。そうでなければundefined
*/
parent_id?: string;
};
export type CreatePostResponse = CreatedPost;
export const createPost = async (body: CreatePostBody): Promise<CreatePostResponse> => {
return fetchApi('POST', '/posts', { body });
};

export type GetPostsParameters = {
/**
* 取得件数。デフォルト30
*/
limit?: number;
/**
* このIDの投稿より後に投稿されたものを取得する。指定されない場合は、最新のものからlimit件取得する
*/
after?: string;
/**
* リポストのやつを含むかどうか。デフォルトはfalse
*/
repost?: boolean;
};
export type GetPostsResponse = Array<
Expand<
Post & {
/**
* リポストの場合はリポストしたユーザーの名前
*/
repost_user?: string;
}
>
>;
export const getPosts = async ({
limit,
after,
repost,
}: GetPostsParameters): Promise<CreatePostResponse[]> => {
return fetchApi('GET', '/posts', {
parameters: { limit: limit?.toString() ?? '30', after, repost: repost?.toString() ?? 'false' },
});
};

type GetPostResponse = Expand<PostDetail>;
export const getPost = async (postId: string): Promise<GetPostResponse> => {
return fetchApi('GET', `/posts/${postId}`);
};

export type PostReactionResponse = Reaction[];
export const postReaction = async (postId: string, reactionId: number) => {
return fetchApi('POST', `/posts/${postId}/reactions/${reactionId}`);
};

export type DeleteReactionResponse = Reaction[];
export const deleteReaction = async (postId: string, reactionId: number) => {
return fetchApi('DELETE', `/posts/${postId}/reactions/${reactionId}`);
};

export type GetReactionsResponse = ReactionDetail[];
export const getReactions = async (postId: string) => {
return fetchApi('GET', `/posts/${postId}/reactions`);
};

export type GetTrendResponse = Array<Post>;
export const getTrend = async (reactionId: number) => {
return fetchApi('GET', '/trend', { parameters: { reaction_id: reactionId.toString() } });
};

export type GetUserResponse = {
/**
* ユーザーID
*/
user_name: string;
/**
* 投稿数
*/
post_count: number;
/**
* リアクションした数
*/
reaction_count: number;
/**
* リアクションされた数
*/
get_reaction_count: number;
/**
* 投稿のリスト
*/
posts: Post[];
};
export const getUser = async (userName: string) => {
return fetchApi('GET', `/user/${userName}`);
};
Loading

0 comments on commit 38aaae7

Please sign in to comment.