diff --git a/backend/handlers/chat_api.go b/backend/handlers/chat_api.go index 2e3540cb..918d0f3e 100644 --- a/backend/handlers/chat_api.go +++ b/backend/handlers/chat_api.go @@ -47,7 +47,7 @@ func HandleChatRoomHandler(c *websocket.Conn, markerID, reqID string) { defer func() { // On disconnect, remove the client from the room - services.WsRoomManager.RemoveWsFromRoom(markerID, clientId, c) + services.WsRoomManager.RemoveWsFromRoom(markerID, clientId) // services.RemoveConnectionFromRedis(markerID, reqID) // Broadcast leave message diff --git a/backend/handlers/marker_api.go b/backend/handlers/marker_api.go index 1701fc8f..12ef5339 100644 --- a/backend/handlers/marker_api.go +++ b/backend/handlers/marker_api.go @@ -22,7 +22,7 @@ import ( ) var ( - // cache to store encoded marker data + // MarkersLocalCache cache to store encoded marker data MarkersLocalCache []byte // 400 kb is fine here CacheMutex sync.RWMutex ) @@ -76,11 +76,6 @@ func RegisterMarkerRoutes(api fiber.Router) { } func createMarkerWithPhotosHandler(c *fiber.Ctx) error { - // go services.ResetCache(services.ALL_MARKERS_KEY) - CacheMutex.Lock() - MarkersLocalCache = nil - CacheMutex.Unlock() - // Parse the multipart form form, err := c.MultipartForm() if err != nil { @@ -94,21 +89,52 @@ func createMarkerWithPhotosHandler(c *fiber.Ctx) error { } // Location Must Be Inside South Korea - if !util.IsInSouthKoreaPrecisely(latitude, longitude) { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Operations are only allowed within South Korea."}) - } + // if !util.IsInSouthKoreaPrecisely(latitude, longitude) { + // return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Operations are only allowed within South Korea."}) + // } + + errorChan := make(chan error) + defer close(errorChan) + + go func() { + if inSKorea := util.IsInSouthKoreaPrecisely(latitude, longitude); !inSKorea { + errorChan <- fmt.Errorf("operation is only allowed within South Korea") + } else { + errorChan <- nil + } + }() // Checking if there's a marker close to the latitude and longitude - if nearby, _ := services.IsMarkerNearby(latitude, longitude, 10); nearby { - return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "There is a marker already nearby."}) - } + go func() { + if nearby, _ := services.IsMarkerNearby(latitude, longitude, 10); nearby { + errorChan <- fmt.Errorf("there is a marker already nearby") + } else { + errorChan <- nil + } + }() - // Set default description if it's empty or not provided description := GetDescriptionFromForm(form) - if containsBadWord, _ := util.CheckForBadWords(description); containsBadWord { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Comment contains inappropriate content."}) + + // Concurrent check for bad words + go func() { + if containsBadWord, _ := util.CheckForBadWords(description); containsBadWord { + errorChan <- fmt.Errorf("comment contains inappropriate content") + } else { + errorChan <- nil + } + }() + + for i := 0; i < 3; i++ { + if err := <-errorChan; err != nil { + return c.Status(mapErrorToStatus(err.Error())).JSON(fiber.Map{"error": err.Error()}) + } } + // no errors + CacheMutex.Lock() + MarkersLocalCache = nil + CacheMutex.Unlock() + userId := c.Locals("userID").(int) marker, err := services.CreateMarkerWithPhotos(&dto.MarkerRequest{ @@ -605,3 +631,14 @@ func GetMarkerIDFromForm(form *multipart.Form) string { } return "" } + +func mapErrorToStatus(errorMessage string) int { + switch errorMessage { + case "There is a marker already nearby.": + return fiber.StatusConflict + case "Comment contains inappropriate content.": + return fiber.StatusBadRequest + default: + return fiber.StatusInternalServerError + } +} diff --git a/backend/main.go b/backend/main.go index 3d13fa69..5516121a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -243,6 +243,7 @@ func setUpMiddlewares(app *fiber.App) { Level: compress.LevelBestSpeed, })) + // ContentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'; object-src 'none'" app.Use(helmet.New(helmet.Config{XSSProtection: "1; mode=block"})) app.Use(limiter.New(limiter.Config{ Next: func(c *fiber.Ctx) bool { @@ -368,8 +369,8 @@ func setUpGlobals() { // Initialize global variables setTokenExpirationTime() - services.AWS_REGION = os.Getenv("AWS_REGION") - services.S3_BUCKET_NAME = os.Getenv("AWS_BUCKET_NAME") + services.AwsRegion = os.Getenv("AWS_REGION") + services.S3BucketName = os.Getenv("AWS_BUCKET_NAME") util.LOGIN_TOKEN_COOKIE = os.Getenv("TOKEN_COOKIE") } @@ -384,7 +385,7 @@ func setTokenExpirationTime() { } // Assign the converted duration to the global variable - services.TOKEN_DURATION = time.Duration(durationInt) * time.Hour + services.TokenDuration = time.Duration(durationInt) * time.Hour } // countAPIs counts the number of APIs in a Fiber app diff --git a/backend/revive.toml b/backend/revive.toml new file mode 100644 index 00000000..f2ca84d9 --- /dev/null +++ b/backend/revive.toml @@ -0,0 +1,29 @@ +ignoreGeneratedHeader = false +severity = "warning" +confidence = 0.8 +errorCode = 0 +warningCode = 0 + +[rule.blank-imports] +[rule.context-as-argument] +[rule.context-keys-type] +[rule.dot-imports] +[rule.error-return] +[rule.error-strings] +[rule.error-naming] +[rule.exported] +[rule.increment-decrement] +[rule.var-naming] +[rule.var-declaration] +[rule.package-comments] +[rule.range] +[rule.receiver-naming] +[rule.time-naming] +[rule.unexported-return] +[rule.indent-error-flow] +[rule.errorf] +[rule.empty-block] +[rule.superfluous-else] +[rule.unused-parameter] +[rule.unreachable-code] +[rule.redefines-builtin-id] \ No newline at end of file diff --git a/backend/services/chat_conn_service.go b/backend/services/chat_conn_service.go index ef55b6e8..e4e445ee 100644 --- a/backend/services/chat_conn_service.go +++ b/backend/services/chat_conn_service.go @@ -24,8 +24,8 @@ func (manager *RoomConnectionManager) SaveConnection(markerID, clientId string, newConn := &ChulbongConn{ Socket: conn, UserID: clientId, - Send: make(chan []byte, 256), // Buffered channel - InActiveChan: make(chan struct{}, 10), + Send: make(chan []byte, 10), // Buffered channel + InActiveChan: make(chan struct{}), } newConn.UpdateLastSeen() @@ -233,7 +233,7 @@ func RemoveConnectionFromRedis(markerID, xRequestID string) { } // RemoveConnection removes a WebSocket connection associated with a id -func (manager *RoomConnectionManager) RemoveWsFromRoom(markerID, clientId string, conn *websocket.Conn) { +func (manager *RoomConnectionManager) RemoveWsFromRoom(markerID, clientId string) { // manager.mu.Lock() // Lock at the start of the method // defer manager.mu.Unlock() // Unlock when the method returns diff --git a/backend/services/chat_process_service.go b/backend/services/chat_process_service.go index cef2c310..200b5f03 100644 --- a/backend/services/chat_process_service.go +++ b/backend/services/chat_process_service.go @@ -10,6 +10,7 @@ import ( "github.com/goccy/go-json" ) +// ProcessMessageFromSubscription processes a message from a Redis subscription func ProcessMessageFromSubscription(msg []byte) { var broadcastMsg dto.BroadcastMessage err := json.Unmarshal(msg, &broadcastMsg) diff --git a/backend/services/chat_service.go b/backend/services/chat_service.go index 3ed8f2cc..331e8435 100644 --- a/backend/services/chat_service.go +++ b/backend/services/chat_service.go @@ -33,7 +33,7 @@ var ( retryCtx, _ = context.WithCancel(context.Background()) ) -var WsRoomManager *RoomConnectionManager = NewRoomConnectionManager() +var WsRoomManager = NewRoomConnectionManager() type ChulbongConn struct { Socket *websocket.Conn @@ -185,7 +185,7 @@ func (manager *RoomConnectionManager) BroadcastMessage(message []byte, userId, r return } - manager.connections.Range(func(key string, conns []*ChulbongConn) bool { + manager.connections.Range(func(_ string, conns []*ChulbongConn) bool { // Iterate over the connections and send the message for _, conn := range conns { select { diff --git a/backend/services/marker_facilities_service.go b/backend/services/marker_facilities_service.go index a59d871d..e2c3150b 100644 --- a/backend/services/marker_facilities_service.go +++ b/backend/services/marker_facilities_service.go @@ -24,14 +24,14 @@ const ( ) var ( - KAKAO_AK = os.Getenv("KAKAO_AK") + KakaoAK = os.Getenv("KAKAO_AK") HTTPClient = &http.Client{ Timeout: 10 * time.Second, // Set a timeout to avoid hanging requests indefinitely } - IS_WATER_URL = os.Getenv("IS_WATER_API") - IS_WATER_KEY = os.Getenv("IS_WATER_API_KEY") + IsWaterURL = os.Getenv("IS_WATER_API") + IsWaterKEY = os.Getenv("IS_WATER_API_KEY") ) // GetFacilitiesByMarkerID retrieves facilities for a given marker ID. @@ -111,7 +111,7 @@ func FetchAddressFromAPI(latitude, longitude float64) (string, error) { return "-1", fmt.Errorf("creating request: %w", err) } - req.Header.Add("Authorization", "KakaoAK "+KAKAO_AK) + req.Header.Add("Authorization", "KakaoAK "+KakaoAK) resp, err := HTTPClient.Do(req) if err != nil { return "-1", fmt.Errorf("executing request: %w", err) @@ -151,7 +151,7 @@ func FetchXYFromAPI(address string) (float64, float64, error) { return 0.0, 0.0, fmt.Errorf("creating request: %w", err) } - req.Header.Add("Authorization", "KakaoAK "+KAKAO_AK) + req.Header.Add("Authorization", "KakaoAK "+KakaoAK) resp, err := HTTPClient.Do(req) if err != nil { return 0.0, 0.0, fmt.Errorf("executing request: %w", err) @@ -199,7 +199,7 @@ func FetchRegionFromAPI(latitude, longitude float64) (string, error) { return "-1", fmt.Errorf("creating request: %w", err) } - req.Header.Add("Authorization", "KakaoAK "+KAKAO_AK) + req.Header.Add("Authorization", "KakaoAK "+KakaoAK) resp, err := HTTPClient.Do(req) if err != nil { return "-1", fmt.Errorf("executing request: %w", err) @@ -275,7 +275,7 @@ func FetchWeatherFromAddress(latitude, longitude float64) (*kakao.WeatherRequest // FetchRegionWaterInfo checks if latitude/longitude is in the water possibly. func FetchRegionWaterInfo(latitude, longitude float64) (bool, error) { - reqURL := fmt.Sprintf("%s?latitude=%f&longitude=%f&rapidapi-key=%s", IS_WATER_URL, latitude, longitude, IS_WATER_KEY) + reqURL := fmt.Sprintf("%s?latitude=%f&longitude=%f&rapidapi-key=%s", IsWaterURL, latitude, longitude, IsWaterKEY) req, err := http.NewRequest(http.MethodGet, reqURL, nil) if err != nil { return false, fmt.Errorf("creating request: %w", err) diff --git a/backend/services/marker_location_service.go b/backend/services/marker_location_service.go index 885fa363..34804553 100644 --- a/backend/services/marker_location_service.go +++ b/backend/services/marker_location_service.go @@ -85,7 +85,7 @@ func FindRankedMarkersInCurrentArea(lat, long float64, distance, limit int) ([]d } ctx := context.Background() - floatMin := float64(MIN_CLICK_RANK) + floatMin := float64(MinClickRank) result, _ := RedisStore.Do(ctx, RedisStore.B().Zmscore().Key("marker_clicks").Member(markerIDs...).Build()).AsFloatSlice() rankedMarkers := make([]dto.MarkerWithDistance, 0, len(result)) diff --git a/backend/services/marker_rank_service.go b/backend/services/marker_rank_service.go index 0f59aac8..b8261d9d 100644 --- a/backend/services/marker_rank_service.go +++ b/backend/services/marker_rank_service.go @@ -20,40 +20,44 @@ import ( ) // 클릭 이벤트를 저장할 임시 저장소 -var clickEventBuffer = csmap.Create( - csmap.WithShardCount[int, int](64), - csmap.WithCustomHasher[int, int](func(key int) uint64 { - // Convert int to a byte slice - bs := make([]byte, 8) - binary.LittleEndian.PutUint64(bs, uint64(key)) - return xxh3.Hash(bs) - }), +var ( + ClickEventBuffer = csmap.Create( + csmap.WithShardCount[int, int](64), + csmap.WithCustomHasher[int, int](func(key int) uint64 { + // Convert int to a byte slice + bs := make([]byte, 8) + binary.LittleEndian.PutUint64(bs, uint64(key)) + return xxh3.Hash(bs) + }), + ) + + SketchedLocations = csmap.Create( + csmap.WithShardCount[string, *hyperloglog.Sketch](64), + csmap.WithCustomHasher[string, *hyperloglog.Sketch](func(key string) uint64 { + return xxh3.HashString(key) + }), + ) ) -var SketchedLocations = csmap.Create( - csmap.WithShardCount[string, *hyperloglog.Sketch](64), - csmap.WithCustomHasher[string, *hyperloglog.Sketch](func(key string) uint64 { - return xxh3.HashString(key) - }), +const ( + RankUpdateTime = 3 * time.Minute + MinClickRank = 5 ) -const RANK_UPDATE_TIME = 3 * time.Minute -const MIN_CLICK_RANK = 5 - // 클릭 이벤트를 버퍼에 추가하는 함수 func BufferClickEvent(markerID int) { // 현재 클릭 수 조회 // 마커 ID가 존재하지 않으면 클릭 수를 1로 설정 - clickEventBuffer.SetIfAbsent(markerID, 1) + ClickEventBuffer.SetIfAbsent(markerID, 1) - actual, ok := clickEventBuffer.Load(markerID) + actual, ok := ClickEventBuffer.Load(markerID) if !ok { return } // 마커 ID가 존재하면 클릭 수를 1 증가 newClicks := actual + 1 - clickEventBuffer.Store(markerID, newClicks) + ClickEventBuffer.Store(markerID, newClicks) } func SaveUniqueVisitor(markerID string, uniqueUser string) { @@ -99,13 +103,13 @@ func GetAllUniqueVisitorCounts() map[string]int { // 정해진 시간 간격마다 클릭 이벤트 배치 처리를 실행하는 함수 func ProcessClickEventsBatch() { // 일정 시간 간격으로 배치 처리 실행 - ticker := time.NewTicker(RANK_UPDATE_TIME) + ticker := time.NewTicker(RankUpdateTime) defer ticker.Stop() // 함수가 반환될 때 ticker를 정지 for range ticker.C { - IncrementMarkerClicks(clickEventBuffer) + IncrementMarkerClicks(ClickEventBuffer) // 처리 후 버퍼 초기화 - clickEventBuffer.Clear() + ClickEventBuffer.Clear() } } @@ -136,7 +140,7 @@ func GetTopMarkers(limit int) []dto.MarkerSimpleWithAddr { ctx := context.Background() // Convert minClickRank to string and prepare for the ZRangeByScore command - minScore := strconv.Itoa(MIN_CLICK_RANK + 1) // "+1" to adjust for exclusive minimum + minScore := strconv.Itoa(MinClickRank + 1) // "+1" to adjust for exclusive minimum // Use ZREVRANGEBYSCORE to get marker IDs in descending order based on score markerScores, err := RedisStore.Do(ctx, RedisStore.B().Zrevrangebyscore(). diff --git a/backend/services/notification_service.go b/backend/services/notification_service.go index 5047fddf..6b0718d6 100644 --- a/backend/services/notification_service.go +++ b/backend/services/notification_service.go @@ -62,7 +62,7 @@ func PostNotification(userId, notificationType, title, message string, metadata } // GetNotifications retrieves notifications for a specific user (unviewed) -func GetNotifications(userId string) ([]NotificationRedis, error) { +func GetNotifications(userID string) ([]NotificationRedis, error) { var notifications []Notification const query = `(SELECT * FROM Notifications WHERE UserId = ? AND Viewed = FALSE @@ -71,7 +71,7 @@ func GetNotifications(userId string) ([]NotificationRedis, error) { (SELECT * FROM Notifications WHERE NotificationType IN ('NewMarker', 'System', 'Other') AND Viewed = FALSE ORDER BY CreatedAt DESC)` - err := database.DB.Select(¬ifications, query, userId) + err := database.DB.Select(¬ifications, query, userID) if err != nil { return nil, err } @@ -89,7 +89,7 @@ func GetNotifications(userId string) ([]NotificationRedis, error) { results[idx] = mapToNotificationRedis(notif) } } else { - viewed, err := IsNotificationViewed(notif.NotificationId, userId) + viewed, err := IsNotificationViewed(notif.NotificationId, userID) if err != nil { errors <- err return diff --git a/backend/services/redis_services.go b/backend/services/redis_services.go index 563ea821..4530ec37 100644 --- a/backend/services/redis_services.go +++ b/backend/services/redis_services.go @@ -98,7 +98,7 @@ func ResetCache(key string) error { func ResetAllCache(pattern string) error { ctx := context.Background() - var cursor uint64 = 0 + var cursor uint64 for { // Build the SCAN command with the current cursor scanCmd := RedisStore.B().Scan().Cursor(cursor).Match(pattern).Count(10).Build() diff --git a/backend/services/s3_service.go b/backend/services/s3_service.go index afabb987..019fcad9 100644 --- a/backend/services/s3_service.go +++ b/backend/services/s3_service.go @@ -14,8 +14,10 @@ import ( "github.com/google/uuid" ) -var AWS_REGION string -var S3_BUCKET_NAME string +var ( + AwsRegion string + S3BucketName string +) func UploadFileToS3(folder string, file *multipart.FileHeader) (string, error) { // Open the uploaded file @@ -27,7 +29,7 @@ func UploadFileToS3(folder string, file *multipart.FileHeader) (string, error) { // Load the AWS credentials cfg, err := config.LoadDefaultConfig(context.TODO(), - config.WithRegion(AWS_REGION), + config.WithRegion(AwsRegion), ) if err != nil { return "", fmt.Errorf("could not load AWS credentials: %w", err) @@ -48,7 +50,7 @@ func UploadFileToS3(folder string, file *multipart.FileHeader) (string, error) { // Upload the file to S3 _, err = s3Client.PutObject(context.TODO(), &s3.PutObjectInput{ - Bucket: &S3_BUCKET_NAME, + Bucket: &S3BucketName, Key: &key, Body: fileData, }) @@ -57,7 +59,7 @@ func UploadFileToS3(folder string, file *multipart.FileHeader) (string, error) { } // Construct the file URL - fileURL := fmt.Sprintf("https://%s.s3.amazonaws.com/%s", S3_BUCKET_NAME, key) + fileURL := fmt.Sprintf("https://%s.s3.amazonaws.com/%s", S3BucketName, key) return fileURL, nil } @@ -78,7 +80,7 @@ func DeleteDataFromS3(dataURL string) error { key = strings.TrimPrefix(parsedURL.Path, "/") } else { // It's not a valid URL, treat it as a key - bucketName = S3_BUCKET_NAME + bucketName = S3BucketName key = dataURL } @@ -88,7 +90,7 @@ func DeleteDataFromS3(dataURL string) error { // Load the AWS credentials cfg, err := config.LoadDefaultConfig(context.TODO(), - config.WithRegion(AWS_REGION), + config.WithRegion(AwsRegion), ) if err != nil { return fmt.Errorf("could not load AWS credentials: %w", err) @@ -144,7 +146,7 @@ func FetchAllPhotoURLsFromDB() ([]string, error) { func ListAllObjectsInS3() ([]string, error) { // Load the AWS credentials cfg, err := config.LoadDefaultConfig(context.TODO(), - config.WithRegion(AWS_REGION), + config.WithRegion(AwsRegion), ) if err != nil { return nil, fmt.Errorf("error loading AWS config: %w", err) @@ -152,7 +154,7 @@ func ListAllObjectsInS3() ([]string, error) { s3Client := s3.NewFromConfig(cfg) input := &s3.ListObjectsV2Input{ - Bucket: &S3_BUCKET_NAME, + Bucket: &S3BucketName, } var s3Keys []string diff --git a/backend/services/scheduler_service.go b/backend/services/scheduler_service.go index d0dec96b..d50d1038 100644 --- a/backend/services/scheduler_service.go +++ b/backend/services/scheduler_service.go @@ -19,7 +19,7 @@ func RunAllCrons() { CronResetClickRanking() CronOrphanedPhotosCleanup() CronCleanUpOldDirs() - CronProcessClickEventsBatch(RANK_UPDATE_TIME) + CronProcessClickEventsBatch(RankUpdateTime) } // CronService holds a reference to a cron scheduler and its related setup. @@ -115,7 +115,7 @@ func CronProcessClickEventsBatch(interval time.Duration) { // spec = "*/1 * * * *" _, err := c.Schedule(spec, func() { - IncrementMarkerClicks(clickEventBuffer) + IncrementMarkerClicks(ClickEventBuffer) // 처리 후 버퍼 초기화 // clickEventBuffer.Clear() }) diff --git a/backend/services/smtp_service.go b/backend/services/smtp_service.go index 70ec06f4..abfe3803 100644 --- a/backend/services/smtp_service.go +++ b/backend/services/smtp_service.go @@ -191,8 +191,8 @@ func SendPasswordResetEmail(to, token string) error { headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: Password Reset for chulbong-kr\r\nMIME-Version: 1.0;\r\nContent-Type: text/html; charset=\"UTF-8\";\r\n\r\n", smtpUsername, to) // Replace the {{RESET_LINK}} placeholder with the actual reset link - clientUrl := fmt.Sprintf("%s?token=%s&email=%s", frontendResetRouter, token, to) - htmlBody := strings.Replace(emailTemplateForReset, "{{RESET_LINK}}", clientUrl, -1) + clientURL := fmt.Sprintf("%s?token=%s&email=%s", frontendResetRouter, token, to) + htmlBody := strings.Replace(emailTemplateForReset, "{{RESET_LINK}}", clientURL, -1) // Combine headers and HTML body into a single raw email message message := []byte(headers + htmlBody) diff --git a/backend/services/token_service.go b/backend/services/token_service.go index 76dd3fdc..e0207a3f 100644 --- a/backend/services/token_service.go +++ b/backend/services/token_service.go @@ -14,7 +14,7 @@ func GenerateAndSaveToken(userID int) (string, error) { return "", err } - expiresAt := time.Now().Add(TOKEN_DURATION) // Use the global duration. + expiresAt := time.Now().Add(TokenDuration) // Use the global duration. err = SaveOrUpdateOpaqueToken(userID, token, expiresAt) if err != nil { return "", err diff --git a/backend/services/user_service.go b/backend/services/user_service.go index 782ccd87..76aabdaa 100644 --- a/backend/services/user_service.go +++ b/backend/services/user_service.go @@ -22,8 +22,8 @@ import ( ) var ( - TOKEN_DURATION time.Duration - NAVER_EMAIL_VERIFY_URL = os.Getenv("NAVER_EMAIL_VERIFY_URL") + TokenDuration time.Duration + NaverEmailVerifyURL = os.Getenv("NAVER_EMAIL_VERIFY_URL") userRepo = repository.NewUserRepository() ) @@ -364,7 +364,7 @@ func GetUserFromContext(c *fiber.Ctx) (*dto.UserData, error) { // VerifyNaverEmail can check naver email existence before sending func VerifyNaverEmail(naverAddress string) (bool, error) { naverAddress = strings.Split(naverAddress, "@naver.com")[0] - reqURL := fmt.Sprintf("%s=%s", NAVER_EMAIL_VERIFY_URL, naverAddress) + reqURL := fmt.Sprintf("%s=%s", NaverEmailVerifyURL, naverAddress) req, err := http.NewRequest(http.MethodGet, reqURL, nil) if err != nil { return false, fmt.Errorf("creating request: %w", err) @@ -393,9 +393,9 @@ func VerifyNaverEmail(naverAddress string) (bool, error) { // Check if the body is non-empty and ends with 'N' if len(bodyString) > 0 && bodyString[len(bodyString)-1] == 'N' { return true, nil - } else { - return false, nil } + + return false, nil } // private