构建一个即时消息应用(四):消息 | Linux 中国

文章图片
在这篇文章中 , 我们将对端点进行编码 , 以创建一条消息并列出它们 , 同时还将编写一个端点以更新参与者上次阅读消息的时间 。
?来源:linux.cn?作者:NicolásParada?译者:XianLeiGao?
(本文字数:7533 , 阅读时长大约:8分钟)
本文是该系列的第四篇 。
【构建一个即时消息应用(四):消息 | Linux 中国】第一篇:模式
第二篇:OAuth
第三篇:对话
在这篇文章中 , 我们将对端点进行编码 , 以创建一条消息并列出它们 , 同时还将编写一个端点以更新参与者上次阅读消息的时间 。 首先在main()函数中添加这些路由 。
router.HandleFunc(''POST'',''/api/conversations/:conversationID/messages'',requireJSON(guard(createMessage)))router.HandleFunc(''GET'',''/api/conversations/:conversationID/messages'',guard(getMessages))router.HandleFunc(''POST'',''/api/conversations/:conversationID/read_messages'',guard(readMessages))
消息会进入对话 , 因此端点包含对话ID 。
创建消息
该端点处理对/api/conversations/{conversationID}/messages的POST请求 , 其JSON主体仅包含消息内容 , 并返回新创建的消息 。 它有两个副作用:更新对话last_message_id以及更新参与者messages_read_at 。
funccreateMessage(whttp.ResponseWriter,r*http.Request){varinputstruct{Contentstring`json:''content''`}deferr.Body.Close()iferr:=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)ifinput.Content==''''{errs[''content'']=''Messagecontentrequired''}elseiflen([]rune(input.Content))>480{errs[''content'']=''Messagetoolong.480max''}iflen(errs)!=0{respond(w,Errors{errs},http.StatusUnprocessableEntity)return}ctx:=r.Context()authUserID:=ctx.Value(keyAuthUserID).(string)conversationID:=way.Param(ctx,''conversationID'')tx,err:=db.BeginTx(ctx,nil)iferr!=nil{respondError(w,fmt.Errorf(''couldnotbegintx:%v'',err))return}defertx.Rollback()isParticipant,err:=queryParticipantExistance(ctx,tx,authUserID,conversationID)iferr!=nil{respondError(w,fmt.Errorf(''couldnotqueryparticipantexistance:%v'',err))return}if!isParticipant{http.Error(w,''Conversationnotfound'',http.StatusNotFound)return}varmessageMessageiferr:=tx.QueryRowContext(ctx,`INSERTINTOmessages(content,user_id,conversation_id)VALUES($1,$2,$3)RETURNINGid,created_at`,input.Content,authUserID,conversationID).Scan(&message.ID,&message.CreatedAt,);err!=nil{respondError(w,fmt.Errorf(''couldnotinsertmessage:%v'',err))return}if_,err:=tx.ExecContext(ctx,`UPDATEconversationsSETlast_message_id=$1WHEREid=$2`,message.ID,conversationID);err!=nil{respondError(w,fmt.Errorf(''couldnotupdateconversationlastmessageID:%v'',err))return}iferr=tx.Commit();err!=nil{respondError(w,fmt.Errorf(''couldnotcommittxtocreateamessage:%v'',err))return}gofunc(){iferr=updateMessagesReadAt(nil,authUserID,conversationID);err!=nil{log.Printf(''couldnotupdatemessagesreadat:%vn'',err)}}()message.Content=input.Contentmessage.UserID=authUserIDmessage.ConversationID=conversationID//TODO:notifyaboutnewmessage.message.Mine=truerespond(w,message,http.StatusCreated)}
首先 , 它将请求正文解码为包含消息内容的结构 。 然后 , 它验证内容不为空并且少于480个字符 。
varrxSpaces=regexp.MustCompile(''s+'')funcremoveSpaces(sstring)string{ifs==''''{returns}lines:=make([]string,0)for_,line:=rangestrings.Split(s,''n''){line=rxSpaces.ReplaceAllLiteralString(line,'''')line=strings.TrimSpace(line)ifline!=''''{lines=append(lines,line)}}returnstrings.Join(lines,''n'')}
这是删除空格的函数 。 它遍历每一行 , 删除两个以上的连续空格 , 然后回非空行 。
验证之后 , 它将启动一个SQL事务 。 首先 , 它查询对话中的参与者是否存在 。
funcqueryParticipantExistance(ctxcontext.Context,tx*sql.Tx,userID,conversationIDstring)(bool,error){ifctx==nil{ctx=context.Background()}varexistsbooliferr:=tx.QueryRowContext(ctx,`SELECTEXISTS(SELECT1FROMparticipantsWHEREuser_id=$1ANDconversation_id=$2)`,userID,conversationID).Scan(&exists);err!=nil{returnfalse,err}returnexists,nil}
我将其提取到一个函数中 , 因为稍后可以重用 。
如果用户不是对话参与者 , 我们将返回一个404NOTFound错误 。
然后 , 它插入消息并更新对话last_message_id 。 从这时起 , 由于我们不允许删除消息 , 因此last_message_id不能为NULL 。
接下来提交事务 , 并在goroutine中更新参与者messages_read_at 。
funcupdateMessagesReadAt(ctxcontext.Context,userID,conversationIDstring)error{ifctx==nil{ctx=context.Background()}if_,err:=db.ExecContext(ctx,`UPDATEparticipantsSETmessages_read_at=now()WHEREuser_id=$1ANDconversation_id=$2`,userID,conversationID);err!=nil{returnerr}returnnil}
在回复这条新消息之前 , 我们必须通知一下 。 这是我们将要在下一篇文章中编写的实时部分 , 因此我在那里留一了个注释 。
获取消息
这个端点处理对/api/conversations/{conversationID}/messages的GET请求 。 它用一个包含会话中所有消息的JSON数组进行响应 。 它还具有更新参与者messages_read_at的副作用 。
funcgetMessages(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()authUserID:=ctx.Value(keyAuthUserID).(string)conversationID:=way.Param(ctx,''conversationID'')tx,err:=db.BeginTx(ctx,&sql.TxOptions{ReadOnly:true})iferr!=nil{respondError(w,fmt.Errorf(''couldnotbegintx:%v'',err))return}defertx.Rollback()isParticipant,err:=queryParticipantExistance(ctx,tx,authUserID,conversationID)iferr!=nil{respondError(w,fmt.Errorf(''couldnotqueryparticipantexistance:%v'',err))return}if!isParticipant{http.Error(w,''Conversationnotfound'',http.StatusNotFound)return}rows,err:=tx.QueryContext(ctx,`SELECTid,content,created_at,user_id=$1ASmineFROMmessagesWHEREmessages.conversation_id=$2ORDERBYmessages.created_atDESC`,authUserID,conversationID)iferr!=nil{respondError(w,fmt.Errorf(''couldnotquerymessages:%v'',err))return}deferrows.Close()messages:=make([]Message,0)forrows.Next(){varmessageMessageiferr=rows.Scan(&message.ID,&message.Content,&message.CreatedAt,&message.Mine,);err!=nil{respondError(w,fmt.Errorf(''couldnotscanmessage:%v'',err))return}messages=append(messages,message)}iferr=rows.Err();err!=nil{respondError(w,fmt.Errorf(''couldnotiterateovermessages:%v'',err))return}iferr=tx.Commit();err!=nil{respondError(w,fmt.Errorf(''couldnotcommittxtogetmessages:%v'',err))return}gofunc(){iferr=updateMessagesReadAt(nil,authUserID,conversationID);err!=nil{log.Printf(''couldnotupdatemessagesreadat:%vn'',err)}}()respond(w,messages,http.StatusOK)}
首先 , 它以只读模式开始一个SQL事务 。 检查参与者是否存在 , 并查询所有消息 。 在每条消息中 , 我们使用当前经过身份验证的用户ID来了解用户是否拥有该消息(mine) 。 然后 , 它提交事务 , 在goroutine中更新参与者messages_read_at并以消息响应 。
读取消息
该端点处理对/api/conversations/{conversationID}/read_messages的POST请求 。 没有任何请求或响应主体 。 在前端 , 每次有新消息到达实时流时 , 我们都会发出此请求 。
funcreadMessages(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()authUserID:=ctx.Value(keyAuthUserID).(string)conversationID:=way.Param(ctx,''conversationID'')iferr:=updateMessagesReadAt(ctx,authUserID,conversationID);err!=nil{respondError(w,fmt.Errorf(''couldnotupdatemessagesreadat:%v'',err))return}w.WriteHeader(http.StatusNoContent)}
它使用了与更新参与者messages_read_at相同的函数 。
到此为止 。 实时消息是后台仅剩的部分了 。 请等待下一篇文章 。
源代码
via:nicolasparada.netlify.com
作者:NicolásParada选题:lujun9972译者:gxlct008校对:wxy
本文由LCTT原创编译 , Linux中国荣誉推出
推荐阅读
- 猪身上一个部位,无筋无骨不塞牙,比排骨受欢迎,上桌立马被抢光
- 红豆和玉米面是绝配,一个做皮一个做馅,包好入锅一蒸,特香
- 天暖了,趁着这个功夫我们做一个芝麻馅,不煎不炸,蒸熟后包上芝麻糖馅,糯糯甜甜,真好吃
- 肺部|肺不好,会出现“1粗、2痛、3多”,占一个,也最好去查肺CT
- 吸烟|想戒烟反复几次不成功,告你一个简单有效的方法
- 简单快手又下饭的芹菜肉末炒粉丝,吃起来那叫一个香
- 冬天就适合用此物烙饼,鲜香营养,孩子爱吃,早餐吃一个暖心暖胃
- 地球|古代地球是一个水世界吗?
- 中国镇|你知道中国的“心脏”在哪吗?经国家地理测量,位于一个村庄上
- 满满都是营养,一口一个,吃起来太过瘾,这菜嫩到筷子都夹不住!
