mirror of
https://github.com/LCTT/TranslateProject.git
synced 2025-02-03 23:40:14 +08:00
PRF
@gxlct008 很好,感谢接上这个系列~
This commit is contained in:
parent
d458281f9c
commit
3bb56668f8
@ -1,6 +1,6 @@
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: (gxlct008)
|
||||
[#]: reviewer: ( )
|
||||
[#]: reviewer: (wxy)
|
||||
[#]: publisher: ( )
|
||||
[#]: url: ( )
|
||||
[#]: subject: (Building a Messenger App: Messages)
|
||||
@ -14,10 +14,9 @@
|
||||
|
||||
* [第一篇: 模式][1]
|
||||
* [第二篇: OAuth][2]
|
||||
* [第三篇: 对话Conversations][3]
|
||||
* [第三篇: 对话][3]
|
||||
|
||||
|
||||
在这篇文章中,我们将对端点进行编码以创建一条消息并列出它们,同时还将编写一个端点以更新参与者上次阅读消息的时间。 首先在 `main()` 函数中添加这些路由。
|
||||
在这篇文章中,我们将对端点进行编码,以创建一条消息并列出它们,同时还将编写一个端点以更新参与者上次阅读消息的时间。 首先在 `main()` 函数中添加这些路由。
|
||||
|
||||
```
|
||||
router.HandleFunc("POST", "/api/conversations/:conversationID/messages", requireJSON(guard(createMessage)))
|
||||
@ -25,138 +24,138 @@ router.HandleFunc("GET", "/api/conversations/:conversationID/messages", guard(ge
|
||||
router.HandleFunc("POST", "/api/conversations/:conversationID/read_messages", guard(readMessages))
|
||||
```
|
||||
|
||||
消息进入对话,因此端点包含对话 ID。
|
||||
消息会进入对话,因此端点包含对话 ID。
|
||||
|
||||
### 创建消息
|
||||
|
||||
该端点使用仅包含消息内容的 JSON 主体处理对 `/api/conversations/{conversationID}/messages` 的 POST 请求,并返回新创建的消息。 它有两个副作用:更新对话 `last_message_id` 以及更新参与者 `messages_read_at`。
|
||||
该端点处理对 `/api/conversations/{conversationID}/messages` 的 POST 请求,其 JSON 主体仅包含消息内容,并返回新创建的消息。它有两个副作用:更新对话 `last_message_id` 以及更新参与者 `messages_read_at`。
|
||||
|
||||
```
|
||||
func createMessage(w http.ResponseWriter, r *http.Request) {
|
||||
var input struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
errs := make(map[string]string)
|
||||
input.Content = removeSpaces(input.Content)
|
||||
if input.Content == "" {
|
||||
errs["content"] = "Message content required"
|
||||
} else if len([]rune(input.Content)) > 480 {
|
||||
errs["content"] = "Message too long. 480 max"
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
respond(w, Errors{errs}, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
errs := make(map[string]string)
|
||||
input.Content = removeSpaces(input.Content)
|
||||
if input.Content == "" {
|
||||
errs["content"] = "Message content required"
|
||||
} else if len([]rune(input.Content)) > 480 {
|
||||
errs["content"] = "Message too long. 480 max"
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
respond(w, Errors{errs}, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
isParticipant, err := queryParticipantExistance(ctx, tx, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query participant existance: %v", err))
|
||||
return
|
||||
}
|
||||
isParticipant, err := queryParticipantExistance(ctx, tx, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query participant existance: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !isParticipant {
|
||||
http.Error(w, "Conversation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !isParticipant {
|
||||
http.Error(w, "Conversation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var message Message
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
INSERT INTO messages (content, user_id, conversation_id) VALUES
|
||||
($1, $2, $3)
|
||||
RETURNING id, created_at
|
||||
`, input.Content, authUserID, conversationID).Scan(
|
||||
&message.ID,
|
||||
&message.CreatedAt,
|
||||
); err != nil {
|
||||
respondError(w, fmt.Errorf("could not insert message: %v", err))
|
||||
return
|
||||
}
|
||||
var message Message
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
INSERT INTO messages (content, user_id, conversation_id) VALUES
|
||||
($1, $2, $3)
|
||||
RETURNING id, created_at
|
||||
`, input.Content, authUserID, conversationID).Scan(
|
||||
&message.ID,
|
||||
&message.CreatedAt,
|
||||
); err != nil {
|
||||
respondError(w, fmt.Errorf("could not insert message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE conversations SET last_message_id = $1
|
||||
WHERE id = $2
|
||||
`, message.ID, conversationID); err != nil {
|
||||
respondError(w, fmt.Errorf("could not update conversation last message ID: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE conversations SET last_message_id = $1
|
||||
WHERE id = $2
|
||||
`, message.ID, conversationID); err != nil {
|
||||
respondError(w, fmt.Errorf("could not update conversation last message ID: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not commit tx to create a message: %v", err))
|
||||
return
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not commit tx to create a message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = updateMessagesReadAt(nil, authUserID, conversationID); err != nil {
|
||||
log.Printf("could not update messages read at: %v\n", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
if err = updateMessagesReadAt(nil, authUserID, conversationID); err != nil {
|
||||
log.Printf("could not update messages read at: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
message.Content = input.Content
|
||||
message.UserID = authUserID
|
||||
message.ConversationID = conversationID
|
||||
// TODO: notify about new message.
|
||||
message.Mine = true
|
||||
message.Content = input.Content
|
||||
message.UserID = authUserID
|
||||
message.ConversationID = conversationID
|
||||
// TODO: notify about new message.
|
||||
message.Mine = true
|
||||
|
||||
respond(w, message, http.StatusCreated)
|
||||
respond(w, message, http.StatusCreated)
|
||||
}
|
||||
```
|
||||
|
||||
首先,它将请求正文解码为具有消息内容的结构。然后,它验证内容不为空并且少于 480 个字符。
|
||||
首先,它将请求正文解码为包含消息内容的结构。然后,它验证内容不为空并且少于 480 个字符。
|
||||
|
||||
```
|
||||
var rxSpaces = regexp.MustCompile("\\s+")
|
||||
|
||||
func removeSpaces(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
lines := make([]string, 0)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
line = rxSpaces.ReplaceAllLiteralString(line, " ")
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
lines := make([]string, 0)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
line = rxSpaces.ReplaceAllLiteralString(line, " ")
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
```
|
||||
|
||||
这是删除空格的函数。它迭代每一行,删除两个以上的连续空格,然后回非空行。
|
||||
这是删除空格的函数。它遍历每一行,删除两个以上的连续空格,然后回非空行。
|
||||
|
||||
验证之后,它将启动一个 SQL 事务。首先,它查询对话中的参与者是否存在。
|
||||
|
||||
```
|
||||
func queryParticipantExistance(ctx context.Context, tx *sql.Tx, userID, conversationID string) (bool, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var exists bool
|
||||
if err := tx.QueryRowContext(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM participants
|
||||
WHERE user_id = $1 AND conversation_id = $2
|
||||
)`, userID, conversationID).Scan(&exists); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var exists bool
|
||||
if err := tx.QueryRowContext(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM participants
|
||||
WHERE user_id = $1 AND conversation_id = $2
|
||||
)`, userID, conversationID).Scan(&exists); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
```
|
||||
|
||||
@ -170,120 +169,120 @@ func queryParticipantExistance(ctx context.Context, tx *sql.Tx, userID, conversa
|
||||
|
||||
```
|
||||
func updateMessagesReadAt(ctx context.Context, userID, conversationID string) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
UPDATE participants SET messages_read_at = now()
|
||||
WHERE user_id = $1 AND conversation_id = $2
|
||||
`, userID, conversationID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
UPDATE participants SET messages_read_at = now()
|
||||
WHERE user_id = $1 AND conversation_id = $2
|
||||
`, userID, conversationID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
在回复这条新消息之前,我们必须通知它。 这是我们将要在下一篇文章中编写的实时部分,因此我在那里留一个注释。
|
||||
在回复这条新消息之前,我们必须通知一下。这是我们将要在下一篇文章中编写的实时部分,因此我在那里留一了个注释。
|
||||
|
||||
### 获取消息
|
||||
|
||||
这个端点处理对 `/api/conversations/{conversationID}/messages` 的 GET 请求。 它用一个包含会话中所有消息的 JSON 数组进行响应。 它还具有更新参与者 `messages_read_at` 的副作用。
|
||||
这个端点处理对 `/api/conversations/{conversationID}/messages` 的 GET 请求。 它用一个包含会话中所有消息的 JSON 数组进行响应。它还具有更新参与者 `messages_read_at` 的副作用。
|
||||
|
||||
```
|
||||
func getMessages(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
isParticipant, err := queryParticipantExistance(ctx, tx, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query participant existance: %v", err))
|
||||
return
|
||||
}
|
||||
isParticipant, err := queryParticipantExistance(ctx, tx, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query participant existance: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !isParticipant {
|
||||
http.Error(w, "Conversation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !isParticipant {
|
||||
http.Error(w, "Conversation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
content,
|
||||
created_at,
|
||||
user_id = $1 AS mine
|
||||
FROM messages
|
||||
WHERE messages.conversation_id = $2
|
||||
ORDER BY messages.created_at DESC
|
||||
`, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query messages: %v", err))
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
content,
|
||||
created_at,
|
||||
user_id = $1 AS mine
|
||||
FROM messages
|
||||
WHERE messages.conversation_id = $2
|
||||
ORDER BY messages.created_at DESC
|
||||
`, authUserID, conversationID)
|
||||
if err != nil {
|
||||
respondError(w, fmt.Errorf("could not query messages: %v", err))
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
messages := make([]Message, 0)
|
||||
for rows.Next() {
|
||||
var message Message
|
||||
if err = rows.Scan(
|
||||
&message.ID,
|
||||
&message.Content,
|
||||
&message.CreatedAt,
|
||||
&message.Mine,
|
||||
); err != nil {
|
||||
respondError(w, fmt.Errorf("could not scan message: %v", err))
|
||||
return
|
||||
}
|
||||
messages := make([]Message, 0)
|
||||
for rows.Next() {
|
||||
var message Message
|
||||
if err = rows.Scan(
|
||||
&message.ID,
|
||||
&message.Content,
|
||||
&message.CreatedAt,
|
||||
&message.Mine,
|
||||
); err != nil {
|
||||
respondError(w, fmt.Errorf("could not scan message: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
messages = append(messages, message)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not iterate over messages: %v", err))
|
||||
return
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not iterate over messages: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not commit tx to get messages: %v", err))
|
||||
return
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
respondError(w, fmt.Errorf("could not commit tx to get messages: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = updateMessagesReadAt(nil, authUserID, conversationID); err != nil {
|
||||
log.Printf("could not update messages read at: %v\n", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
if err = updateMessagesReadAt(nil, authUserID, conversationID); err != nil {
|
||||
log.Printf("could not update messages read at: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
respond(w, messages, http.StatusOK)
|
||||
respond(w, messages, http.StatusOK)
|
||||
}
|
||||
```
|
||||
|
||||
首先,它以只读模式开始一个 SQL 事务。 检查参与者是否存在并查询所有消息。 在每条消息中,我们使用当前经过身份验证的用户 ID 来了解用户是否拥有该消息(`我的`)。 然后,它提交事务,在 goroutine 中更新参与者 `messages_read_at` 并以消息响应。
|
||||
首先,它以只读模式开始一个 SQL 事务。检查参与者是否存在,并查询所有消息。在每条消息中,我们使用当前经过身份验证的用户 ID 来了解用户是否拥有该消息(`mine`)。 然后,它提交事务,在 goroutine 中更新参与者 `messages_read_at` 并以消息响应。
|
||||
|
||||
### 阅读消息
|
||||
### 读取消息
|
||||
|
||||
该端点处理对 `/api/conversations/{conversationID}/read_messages` 的 POST 请求。 没有任何请求或响应主体。 在前端,每次有新消息到达实时流时,我们都会发出此请求。
|
||||
|
||||
```
|
||||
func readMessages(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
ctx := r.Context()
|
||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||
conversationID := way.Param(ctx, "conversationID")
|
||||
|
||||
if err := updateMessagesReadAt(ctx, authUserID, conversationID); err != nil {
|
||||
respondError(w, fmt.Errorf("could not update messages read at: %v", err))
|
||||
return
|
||||
}
|
||||
if err := updateMessagesReadAt(ctx, authUserID, conversationID); err != nil {
|
||||
respondError(w, fmt.Errorf("could not update messages read at: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
```
|
||||
|
||||
@ -293,7 +292,7 @@ func readMessages(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
到此为止。实时消息是后台仅剩的部分了。请等待下一篇文章。
|
||||
|
||||
[Souce Code][4]
|
||||
- [源代码][4]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@ -301,14 +300,14 @@ via: https://nicolasparada.netlify.com/posts/go-messenger-messages/
|
||||
|
||||
作者:[Nicolás Parada][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[译者ID](https://github.com/gxlct008)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
译者:[gxlct008](https://github.com/gxlct008)
|
||||
校对:[wxy](https://github.com/wxy)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://nicolasparada.netlify.com/
|
||||
[b]: https://github.com/lujun9972
|
||||
[1]: https://nicolasparada.netlify.com/posts/go-messenger-schema/
|
||||
[2]: https://nicolasparada.netlify.com/posts/go-messenger-oauth/
|
||||
[3]: https://nicolasparada.netlify.com/posts/go-messenger-conversations/
|
||||
[1]: https://linux.cn/article-11396-1.html
|
||||
[2]: https://linux.cn/article-11510-1.html
|
||||
[3]: https://linux.cn/article-12056-1.html
|
||||
[4]: https://github.com/nicolasparada/go-messenger-demo
|
||||
|
Loading…
Reference in New Issue
Block a user