├── legacy ├── line │ ├── __init__.py │ ├── utils │ │ ├── __init__.py │ │ └── cache.py │ ├── webhook │ │ ├── __init__.py │ │ └── base.py │ ├── apps.py │ ├── constants.py │ ├── tasks.py │ └── services │ │ └── __init__.py └── organization │ ├── __init__.py │ ├── apps.py │ ├── constants.py │ └── models.py ├── python_src ├── tests │ ├── __init__.py │ └── domain │ │ ├── __init__.py │ │ └── common │ │ ├── __init__.py │ │ └── test_error_code.py ├── .python-version ├── internal │ ├── __init__.py │ ├── router │ │ ├── __init__.py │ │ ├── handlers.py │ │ └── middleware.py │ └── domain │ │ ├── common │ │ ├── __init__.py │ │ ├── requestid.py │ │ ├── error_code.py │ │ └── error.py │ │ ├── __init__.py │ │ ├── organization │ │ ├── __init__.py │ │ ├── organization.py │ │ ├── business_hour.py │ │ └── bot.py │ │ └── auto_reply │ │ ├── __init__.py │ │ ├── auto_reply.py │ │ └── webhook_trigger.py ├── entrypoint │ ├── __init__.py │ └── app │ │ ├── __init__.py │ │ ├── settings.py │ │ └── http_server.py ├── env.example └── pyproject.toml ├── cheat_sheet ├── python │ ├── 2_extend_feature │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── domain │ │ │ │ ├── __init__.py │ │ │ │ └── common │ │ │ │ ├── __init__.py │ │ │ │ └── test_error_code.py │ │ ├── .python-version │ │ ├── internal │ │ │ ├── __init__.py │ │ │ ├── router │ │ │ │ ├── __init__.py │ │ │ │ ├── handlers.py │ │ │ │ └── middleware.py │ │ │ └── domain │ │ │ │ ├── common │ │ │ │ ├── __init__.py │ │ │ │ ├── requestid.py │ │ │ │ ├── error_code.py │ │ │ │ └── error.py │ │ │ │ ├── __init__.py │ │ │ │ ├── organization │ │ │ │ ├── __init__.py │ │ │ │ ├── organization.py │ │ │ │ ├── business_hour.py │ │ │ │ └── bot.py │ │ │ │ └── auto_reply │ │ │ │ ├── auto_reply.py │ │ │ │ ├── __init__.py │ │ │ │ └── webhook_event.py │ │ ├── entrypoint │ │ │ ├── __init__.py │ │ │ └── app │ │ │ │ ├── __init__.py │ │ │ │ ├── settings.py │ │ │ │ └── http_server.py │ │ ├── env.example │ │ └── pyproject.toml │ └── 1_rewrite_brownfield │ │ ├── .python-version │ │ ├── tests │ │ ├── __init__.py │ │ └── domain │ │ │ ├── __init__.py │ │ │ └── common │ │ │ └── __init__.py │ │ ├── internal │ │ ├── __init__.py │ │ ├── router │ │ │ ├── __init__.py │ │ │ ├── handlers.py │ │ │ └── middleware.py │ │ └── domain │ │ │ ├── common │ │ │ ├── __init__.py │ │ │ ├── requestid.py │ │ │ ├── error_code.py │ │ │ └── error.py │ │ │ ├── __init__.py │ │ │ ├── organization │ │ │ ├── __init__.py │ │ │ ├── organization.py │ │ │ ├── business_hour.py │ │ │ └── bot.py │ │ │ └── auto_reply │ │ │ ├── auto_reply.py │ │ │ ├── __init__.py │ │ │ ├── webhook_event.py │ │ │ └── webhook_trigger.py │ │ ├── entrypoint │ │ ├── __init__.py │ │ └── app │ │ │ ├── __init__.py │ │ │ ├── settings.py │ │ │ └── http_server.py │ │ ├── env.example │ │ └── pyproject.toml └── go │ ├── 2_extend_feature │ ├── go.mod │ ├── Makefile │ ├── internal │ │ └── domain │ │ │ ├── organization │ │ │ ├── business_hour.go │ │ │ ├── organization.go │ │ │ └── bot.go │ │ │ └── auto_reply │ │ │ ├── auto_reply.go │ │ │ └── webhook_trigger.go │ └── go.sum │ └── 1_rewrite_brownfield │ ├── go.mod │ ├── Makefile │ ├── internal │ └── domain │ │ ├── organization │ │ ├── business_hour.go │ │ ├── organization.go │ │ └── bot.go │ │ └── auto_reply │ │ ├── auto_reply.go │ │ └── webhook_trigger.go │ └── go.sum ├── cover.png ├── .vscode └── settings.json ├── AI_coding_flow.png ├── brownfield_rewrite.png ├── .gitignore ├── go_src ├── internal │ ├── router │ │ ├── handler_healthcheck.go │ │ ├── validator │ │ │ └── valdator.go │ │ ├── middleware.go │ │ ├── handler.go │ │ ├── middleware_requestid.go │ │ ├── response.go │ │ ├── middleware_zwsp_test.go │ │ ├── middleware_zwsp.go │ │ ├── param_util.go │ │ └── middleware_logger.go │ ├── domain │ │ ├── common │ │ │ ├── requestid_test.go │ │ │ ├── requestid.go │ │ │ ├── error_code.go │ │ │ ├── error.go │ │ │ └── error_test.go │ │ ├── organization │ │ │ ├── business_hour.go │ │ │ ├── organization.go │ │ │ └── bot.go │ │ └── auto_reply │ │ │ ├── auto_reply.go │ │ │ └── webhook_trigger.go │ └── app │ │ └── application.go ├── Makefile ├── go.mod └── cmd │ └── app │ ├── http.go │ └── main.go ├── .github ├── instructions │ └── feature_kb.instructions.md ├── pull_request_template.md └── workflows │ ├── python_ci.yml │ ├── go.yml │ └── bot_update_pr_description.yaml ├── .cursor └── rules │ └── feature_kb.mdc ├── .ai └── prompt │ ├── prd_update.prompt.md │ ├── dev_with_kb.prompt.md │ ├── prd_spec_to_user_story.prompt.md │ └── prd_user_story_to_use_case.prompt.md └── spec └── ig_story.json /legacy/line/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /legacy/line/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /legacy/line/webhook/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /legacy/organization/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python_src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Tests package 2 | -------------------------------------------------------------------------------- /python_src/.python-version: -------------------------------------------------------------------------------- 1 | ai-coding-workshop 2 | -------------------------------------------------------------------------------- /python_src/tests/domain/__init__.py: -------------------------------------------------------------------------------- 1 | # Domain tests package 2 | -------------------------------------------------------------------------------- /python_src/internal/__init__.py: -------------------------------------------------------------------------------- 1 | # Internal application packages 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Tests package 2 | -------------------------------------------------------------------------------- /python_src/entrypoint/__init__.py: -------------------------------------------------------------------------------- 1 | # Command line interface package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/.python-version: -------------------------------------------------------------------------------- 1 | ai-coding-workshop 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Tests package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/.python-version: -------------------------------------------------------------------------------- 1 | ai-coding-workshop 2 | -------------------------------------------------------------------------------- /python_src/tests/domain/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Domain common tests package 2 | -------------------------------------------------------------------------------- /python_src/entrypoint/app/__init__.py: -------------------------------------------------------------------------------- 1 | # Main application entry point package 2 | -------------------------------------------------------------------------------- /python_src/internal/router/__init__.py: -------------------------------------------------------------------------------- 1 | # HTTP routing and middleware package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/tests/domain/__init__.py: -------------------------------------------------------------------------------- 1 | # Domain tests package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/tests/domain/__init__.py: -------------------------------------------------------------------------------- 1 | # Domain tests package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/__init__.py: -------------------------------------------------------------------------------- 1 | # Internal application packages 2 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatbotgang/ai-coding-workshop-250712/HEAD/cover.png -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/__init__.py: -------------------------------------------------------------------------------- 1 | # Internal application packages 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/entrypoint/__init__.py: -------------------------------------------------------------------------------- 1 | # Command line interface package 2 | -------------------------------------------------------------------------------- /python_src/internal/domain/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Common domain utilities and error handling 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/entrypoint/__init__.py: -------------------------------------------------------------------------------- 1 | # Command line interface package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/tests/domain/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Domain common tests package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/tests/domain/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Domain common tests package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/entrypoint/app/__init__.py: -------------------------------------------------------------------------------- 1 | # Main application entry point package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/router/__init__.py: -------------------------------------------------------------------------------- 1 | # HTTP routing and middleware package 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "chat.promptFilesLocations": { 3 | ".ai/prompts": true 4 | } 5 | } -------------------------------------------------------------------------------- /AI_coding_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatbotgang/ai-coding-workshop-250712/HEAD/AI_coding_flow.png -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/entrypoint/app/__init__.py: -------------------------------------------------------------------------------- 1 | # Main application entry point package 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/router/__init__.py: -------------------------------------------------------------------------------- 1 | # HTTP routing and middleware package 2 | -------------------------------------------------------------------------------- /brownfield_rewrite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatbotgang/ai-coding-workshop-250712/HEAD/brownfield_rewrite.png -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Common domain utilities and error handling 2 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Common domain utilities and error handling 2 | -------------------------------------------------------------------------------- /legacy/line/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LineConfig(AppConfig): 5 | name = "line" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | */bin/ 3 | */cover.out 4 | .cursor/rules/personal.mdc 5 | .github/instructions/personal.instructions.md 6 | __pycache__ -------------------------------------------------------------------------------- /legacy/organization/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OrganizationConfig(AppConfig): 5 | name = "organization" 6 | -------------------------------------------------------------------------------- /python_src/internal/domain/__init__.py: -------------------------------------------------------------------------------- 1 | """Domain package.""" 2 | 3 | from internal.domain import auto_reply, common, organization 4 | 5 | __all__ = ["auto_reply", "organization", "common"] 6 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/__init__.py: -------------------------------------------------------------------------------- 1 | """Domain package.""" 2 | 3 | from internal.domain import auto_reply, common, organization 4 | 5 | __all__ = ["auto_reply", "organization", "common"] 6 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/__init__.py: -------------------------------------------------------------------------------- 1 | """Domain package.""" 2 | 3 | from internal.domain import auto_reply, common, organization 4 | 5 | __all__ = ["auto_reply", "organization", "common"] 6 | -------------------------------------------------------------------------------- /go_src/internal/router/handler_healthcheck.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func handlerHealthCheck() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | c.Status(http.StatusOK) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /python_src/env.example: -------------------------------------------------------------------------------- 1 | # Workshop Application Configuration 2 | # Copy this file to .env and adjust values as needed 3 | 4 | # Runtime environment: local, staging, production 5 | WORKSHOP_ENV=local 6 | 7 | # Log level: error, warn, info, debug, disabled 8 | WORKSHOP_LOG_LEVEL=debug 9 | 10 | # HTTP server port (1-65535) 11 | WORKSHOP_PORT=8080 -------------------------------------------------------------------------------- /legacy/organization/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OrganizationReleaseTier(str, Enum): 5 | FIRST_TIER = "1st_tier" 6 | SECOND_TIER = "2nd_tier" 7 | THIRD_TIER = "3rd_tier" 8 | 9 | 10 | class OrganizationServiceLevel(str, Enum): 11 | L1 = "L1" 12 | L2 = "L2" 13 | L3 = "L3" 14 | L4 = "L4" 15 | L5 = "L5" 16 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/env.example: -------------------------------------------------------------------------------- 1 | # Workshop Application Configuration 2 | # Copy this file to .env and adjust values as needed 3 | 4 | # Runtime environment: local, staging, production 5 | WORKSHOP_ENV=local 6 | 7 | # Log level: error, warn, info, debug, disabled 8 | WORKSHOP_LOG_LEVEL=debug 9 | 10 | # HTTP server port (1-65535) 11 | WORKSHOP_PORT=8080 -------------------------------------------------------------------------------- /cheat_sheet/go/2_extend_feature/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chatbotgang/workshop 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/env.example: -------------------------------------------------------------------------------- 1 | # Workshop Application Configuration 2 | # Copy this file to .env and adjust values as needed 3 | 4 | # Runtime environment: local, staging, production 5 | WORKSHOP_ENV=local 6 | 7 | # Log level: error, warn, info, debug, disabled 8 | WORKSHOP_LOG_LEVEL=debug 9 | 10 | # HTTP server port (1-65535) 11 | WORKSHOP_PORT=8080 -------------------------------------------------------------------------------- /go_src/internal/domain/common/requestid_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRequestID(t *testing.T) { 11 | t.Parallel() 12 | rid := NewRequestID() 13 | 14 | ctx := SetRequestID(context.Background(), rid) 15 | assert.EqualValues(t, rid, GetRequestID(ctx)) 16 | } 17 | -------------------------------------------------------------------------------- /cheat_sheet/go/1_rewrite_brownfield/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chatbotgang/workshop 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /python_src/internal/domain/organization/__init__.py: -------------------------------------------------------------------------------- 1 | """Organization domain module.""" 2 | 3 | from internal.domain.organization.bot import Bot, BotType 4 | from internal.domain.organization.business_hour import BusinessHour, WeekDay 5 | from internal.domain.organization.organization import LanguageCode, Organization 6 | 7 | __all__ = ["Bot", "BotType", "BusinessHour", "WeekDay", "Organization", "LanguageCode"] 8 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/organization/__init__.py: -------------------------------------------------------------------------------- 1 | """Organization domain module.""" 2 | 3 | from internal.domain.organization.bot import Bot, BotType 4 | from internal.domain.organization.business_hour import BusinessHour, WeekDay 5 | from internal.domain.organization.organization import LanguageCode, Organization 6 | 7 | __all__ = ["Bot", "BotType", "BusinessHour", "WeekDay", "Organization", "LanguageCode"] 8 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/organization/__init__.py: -------------------------------------------------------------------------------- 1 | """Organization domain module.""" 2 | 3 | from internal.domain.organization.bot import Bot, BotType 4 | from internal.domain.organization.business_hour import BusinessHour, WeekDay 5 | from internal.domain.organization.organization import LanguageCode, Organization 6 | 7 | __all__ = ["Bot", "BotType", "BusinessHour", "WeekDay", "Organization", "LanguageCode"] 8 | -------------------------------------------------------------------------------- /cheat_sheet/go/2_extend_feature/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | GOLANGCI_LINT_VERSION="v2.2.1" 4 | 5 | # Setup test packages 6 | TEST_PACKAGES = ./internal/... 7 | 8 | test: 9 | go test -count=1 -race $(TEST_PACKAGES) 10 | 11 | lint: 12 | @if [ ! -f ./bin/golangci-lint ]; then \ 13 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s $(GOLANGCI_LINT_VERSION); \ 14 | fi; 15 | @echo "golangci-lint checking..." 16 | @./bin/golangci-lint -v run $(TEST_PACKAGES) 17 | -------------------------------------------------------------------------------- /cheat_sheet/go/1_rewrite_brownfield/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | GOLANGCI_LINT_VERSION="v2.2.1" 4 | 5 | # Setup test packages 6 | TEST_PACKAGES = ./internal/... 7 | 8 | test: 9 | go test -count=1 -race $(TEST_PACKAGES) 10 | 11 | lint: 12 | @if [ ! -f ./bin/golangci-lint ]; then \ 13 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s $(GOLANGCI_LINT_VERSION); \ 14 | fi; 15 | @echo "golangci-lint checking..." 16 | @./bin/golangci-lint -v run $(TEST_PACKAGES) 17 | -------------------------------------------------------------------------------- /.github/instructions/feature_kb.instructions.md: -------------------------------------------------------------------------------- 1 | # Feature KB 2 | 3 | > Feature KB is the knowledgebase of product features in the codebase, helping future AI or developers safely modify, extend, or migrate this feature. 4 | 5 | ## KB Documentation 6 | Check ./.ai/kb and ./legacy/kb for all KB docs 7 | 8 | ## KB Extraction 9 | Check ./.ai/prompt/kb_extraction.prompt.md 10 | 11 | ## Development with KB 12 | Check ./.ai/prompt/dev_with_kb.prompt.md 13 | 14 | # KB Update 15 | Check ./.ai/prompt/kb_management.prompt.md 16 | 17 | -------------------------------------------------------------------------------- /go_src/internal/router/validator/valdator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "github.com/gin-gonic/gin/binding" 5 | "github.com/go-playground/validator/v10" 6 | ) 7 | 8 | func RegisteValidator() { 9 | // inject tag text and corresponding function here 10 | validators := map[string]validator.Func{} 11 | 12 | for funcName, function := range validators { 13 | if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 14 | // i think we need to define a error here 15 | _ = v.RegisterValidation(funcName, function) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /go_src/internal/router/middleware.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // SetGeneralMiddlewares add general-purpose middlewares 10 | func SetGeneralMiddlewares(ctx context.Context, ginRouter *gin.Engine) { 11 | ginRouter.Use(gin.Recovery()) 12 | ginRouter.Use(RequestIDMiddleware()) 13 | ginRouter.Use(RemoveZWSPMiddleware()) 14 | 15 | // LoggerMiddleware needs to be after TraceMiddleware, so that it could 16 | // get traceID from the request context 17 | ginRouter.Use(LoggerMiddleware(ctx)) 18 | } 19 | -------------------------------------------------------------------------------- /.cursor/rules/feature_kb.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Rule for developing with KB 3 | alwaysApply: false 4 | --- 5 | # Feature KB 6 | 7 | > Feature KB is the knowledgebase of product features in the codebase, helping future AI or developers safely modify, extend, or migrate this feature. 8 | 9 | ## KB Documentation 10 | Check ./.ai/kb and ./legacy/kb for all KB docs 11 | 12 | ## KB Extraction 13 | Check ./.ai/prompt/kb_extraction.prompt.md 14 | 15 | ## Development with KB 16 | Check ./.ai/prompt/dev_with_kb.prompt.md 17 | 18 | # KB Update 19 | Check ./.ai/prompt/kb_management.prompt.md 20 | 21 | -------------------------------------------------------------------------------- /go_src/internal/domain/common/requestid.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type ctxKey struct{} 10 | 11 | // New returns the new uuid 12 | func NewRequestID() string { 13 | return uuid.New().String() 14 | } 15 | 16 | // Set sets the requestid in the context 17 | func SetRequestID(ctx context.Context, requestID string) context.Context { 18 | return context.WithValue(ctx, ctxKey{}, requestID) 19 | } 20 | 21 | // Get returns the request id 22 | func GetRequestID(ctx context.Context) string { 23 | if rid, ok := ctx.Value(ctxKey{}).(string); ok { 24 | return rid 25 | } 26 | return "" 27 | } 28 | -------------------------------------------------------------------------------- /legacy/line/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | 3 | 4 | class BotType(str, Enum): 5 | LINE = "line" 6 | FB = "fb" 7 | IG = "ig" 8 | 9 | 10 | class WebhookTriggerSettingEventType(IntEnum): 11 | MESSAGE = 1 12 | POSTBACK = 2 13 | FOLLOW = 3 14 | BEACON = 4 15 | TIME = 100 16 | MESSAGE_EDITOR = 101 17 | POSTBACK_EDITOR = 102 18 | 19 | 20 | class WebhookTriggerSettingTriggerScheduleType(str, Enum): 21 | DAILY = "daily" 22 | MONTHLY = "monthly" 23 | BUSINESS_HOUR = "business_hour" 24 | NON_BUSINESS_HOUR = "non_business_hour" 25 | DATE_RANGE = "date_range" 26 | NON_DATE_RANGE = "non_date_range" 27 | -------------------------------------------------------------------------------- /go_src/internal/router/handler.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/chatbotgang/workshop/internal/app" 7 | ) 8 | 9 | // @title workshop API Document 10 | // @version 1.0 11 | 12 | func RegisterHandlers(router *gin.Engine, app *app.Application) { 13 | registerAPIHandlers(router, app) 14 | } 15 | 16 | func registerAPIHandlers(router *gin.Engine, app *app.Application) { 17 | // Build middlewares 18 | // SimpleToken := NewAuthMiddlewareSimple(app) 19 | 20 | // We mount all handlers under /api path 21 | r := router.Group("/api") 22 | v1 := r.Group("/v1") 23 | 24 | // Add health-check 25 | v1.GET("/health", handlerHealthCheck()) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /.ai/prompt/prd_update.prompt.md: -------------------------------------------------------------------------------- 1 | I would like to update user stories and test cases in [the section name where stores user stories and test cases], based on the new conclusions in [the section name where stores meeting notes]. 2 | 3 | Follow the rules: 4 | - Identify any critical clarifications or hidden assumptions that may affect implementation. 5 | - Ask only **one question at a time** to avoid overwhelming the discussion. 6 | - **Do not continue** until the current question is clearly answered. 7 | - Offer **multiple-choice options** when possible to accelerate alignment and decision-making. 8 | - If there are questions that cannot be answered or require broader input, add them to an **"Unresolved Questions"** list for stakeholder review. Do not assume or speculate. 9 | - Keep the original format. -------------------------------------------------------------------------------- /python_src/entrypoint/app/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic import Field 4 | from pydantic_settings import BaseSettings, SettingsConfigDict 5 | 6 | 7 | class AppConfig(BaseSettings): 8 | """Application configuration using pydantic-settings.""" 9 | 10 | model_config = SettingsConfigDict( 11 | env_prefix="WORKSHOP_", env_file=".env", env_file_encoding="utf-8", case_sensitive=False 12 | ) 13 | 14 | env: Literal["local", "staging", "production"] = Field(default="staging", description="The running environment") 15 | 16 | log_level: Literal["error", "warn", "info", "debug", "disabled"] = Field( 17 | default="info", description="Log filtering level" 18 | ) 19 | 20 | port: int = Field(default=8080, description="The HTTP server port", ge=1, le=65535) 21 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/entrypoint/app/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic import Field 4 | from pydantic_settings import BaseSettings, SettingsConfigDict 5 | 6 | 7 | class AppConfig(BaseSettings): 8 | """Application configuration using pydantic-settings.""" 9 | 10 | model_config = SettingsConfigDict( 11 | env_prefix="WORKSHOP_", env_file=".env", env_file_encoding="utf-8", case_sensitive=False 12 | ) 13 | 14 | env: Literal["local", "staging", "production"] = Field(default="staging", description="The running environment") 15 | 16 | log_level: Literal["error", "warn", "info", "debug", "disabled"] = Field( 17 | default="info", description="Log filtering level" 18 | ) 19 | 20 | port: int = Field(default=8080, description="The HTTP server port", ge=1, le=65535) 21 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/entrypoint/app/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic import Field 4 | from pydantic_settings import BaseSettings, SettingsConfigDict 5 | 6 | 7 | class AppConfig(BaseSettings): 8 | """Application configuration using pydantic-settings.""" 9 | 10 | model_config = SettingsConfigDict( 11 | env_prefix="WORKSHOP_", env_file=".env", env_file_encoding="utf-8", case_sensitive=False 12 | ) 13 | 14 | env: Literal["local", "staging", "production"] = Field(default="staging", description="The running environment") 15 | 16 | log_level: Literal["error", "warn", "info", "debug", "disabled"] = Field( 17 | default="info", description="Log filtering level" 18 | ) 19 | 20 | port: int = Field(default=8080, description="The HTTP server port", ge=1, le=65535) 21 | -------------------------------------------------------------------------------- /spec/ig_story.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "18391522015185278", 3 | "caption": null, 4 | "alt_text": null, 5 | "username": "liaolinwu5", 6 | "media_url": "https://scontent-tpe1-1.cdninstagram.com/v/t51.12442-15/514708569_1502601570721241_1893230670798536146_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=106&ccb=1-7&_nc_sid=18de74&_nc_ohc=T8Af7aQrXYwQ7kNvwF2P9sU&_nc_oc=AdmZ1Ojcid5SMTNnRHaF7t1ZLgK_7Aw_h-h4ZMuBlvlipFC-D5ook5HRNgPYVobSENk&_nc_zt=23&_nc_ht=scontent-tpe1-1.cdninstagram.com&edm=AB9oSrcEAAAA&_nc_gid=SLaI14LI-72vZ4aFh9nC6A&oh=00_AfOalHDrbZzzDLhn_sGM5M-lUzUalJhZHSrschVO5yYiDA&oe=686AD575", 7 | "permalink": "https://instagram.com/stories/liaolinwu5/3667537676459255406", 8 | "timestamp": "2025-07-02T02:49:42Z", 9 | "like_count": 0, 10 | "media_type": "IMAGE", 11 | "thumbnail_url": null, 12 | "media_product_type": "STORY" 13 | } -------------------------------------------------------------------------------- /go_src/internal/domain/organization/business_hour.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import "time" 4 | 5 | // Weekday represents the days of the week for business hours. 6 | type Weekday int 7 | 8 | const ( 9 | Monday Weekday = iota + 1 10 | Tuesday 11 | Wednesday 12 | Thursday 13 | Friday 14 | Saturday 15 | Sunday 16 | ) 17 | 18 | // BusinessHour represents the business hours configuration for an organization. 19 | // It defines when the organization is available for business operations. 20 | type BusinessHour struct { 21 | ID int `json:"id"` 22 | OrganizationID int `json:"organization_id"` 23 | Weekday Weekday `json:"weekday"` 24 | StartTime time.Time `json:"start_time"` 25 | EndTime time.Time `json:"end_time"` 26 | CreatedAt time.Time `json:"created_at"` 27 | UpdatedAt time.Time `json:"updated_at"` 28 | } 29 | -------------------------------------------------------------------------------- /cheat_sheet/go/2_extend_feature/internal/domain/organization/business_hour.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import "time" 4 | 5 | // Weekday represents the days of the week for business hours. 6 | type Weekday int 7 | 8 | const ( 9 | Monday Weekday = iota + 1 10 | Tuesday 11 | Wednesday 12 | Thursday 13 | Friday 14 | Saturday 15 | Sunday 16 | ) 17 | 18 | // BusinessHour represents the business hours configuration for an organization. 19 | // It defines when the organization is available for business operations. 20 | type BusinessHour struct { 21 | ID int `json:"id"` 22 | OrganizationID int `json:"organization_id"` 23 | Weekday Weekday `json:"weekday"` 24 | StartTime time.Time `json:"start_time"` 25 | EndTime time.Time `json:"end_time"` 26 | CreatedAt time.Time `json:"created_at"` 27 | UpdatedAt time.Time `json:"updated_at"` 28 | } 29 | -------------------------------------------------------------------------------- /cheat_sheet/go/1_rewrite_brownfield/internal/domain/organization/business_hour.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import "time" 4 | 5 | // Weekday represents the days of the week for business hours. 6 | type Weekday int 7 | 8 | const ( 9 | Monday Weekday = iota + 1 10 | Tuesday 11 | Wednesday 12 | Thursday 13 | Friday 14 | Saturday 15 | Sunday 16 | ) 17 | 18 | // BusinessHour represents the business hours configuration for an organization. 19 | // It defines when the organization is available for business operations. 20 | type BusinessHour struct { 21 | ID int `json:"id"` 22 | OrganizationID int `json:"organization_id"` 23 | Weekday Weekday `json:"weekday"` 24 | StartTime time.Time `json:"start_time"` 25 | EndTime time.Time `json:"end_time"` 26 | CreatedAt time.Time `json:"created_at"` 27 | UpdatedAt time.Time `json:"updated_at"` 28 | } 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 🤔 Why 2 | 3 | 6 | 7 | ## 💡 How 8 | 9 | 14 | 15 | ## 🚀 Demo 16 | 17 | 21 | 22 | ## Check list 23 | - Asana Link: 24 | - [ ] Do you need a feature flag to protect this change? 25 | - [ ] Do you need tests to verify this change? -------------------------------------------------------------------------------- /go_src/internal/app/application.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "sync" 7 | ) 8 | 9 | type Application struct { 10 | Param ApplicationParam 11 | } 12 | 13 | type ApplicationParam struct { 14 | // General configuration 15 | AppName string 16 | Env string 17 | } 18 | 19 | func MustNewApplication(ctx context.Context, wg *sync.WaitGroup, params ApplicationParam) *Application { 20 | app, err := NewApplication(ctx, wg, params) 21 | if err != nil { 22 | log.Panicf("fail to new application, err: %s", err.Error()) 23 | } 24 | return app 25 | } 26 | 27 | func NewApplication(ctx context.Context, wg *sync.WaitGroup, param ApplicationParam) (*Application, error) { 28 | // Create repositories 29 | 30 | // Create servers 31 | 32 | // Create event brokers 33 | 34 | // Create application 35 | app := &Application{ 36 | Param: param, 37 | } 38 | 39 | return app, nil 40 | } 41 | -------------------------------------------------------------------------------- /python_src/internal/domain/common/requestid.py: -------------------------------------------------------------------------------- 1 | """Request ID management utilities.""" 2 | 3 | import uuid 4 | from contextvars import ContextVar 5 | 6 | # Context variable to store request ID 7 | _request_id_var: ContextVar[str] = ContextVar("request_id", default="") 8 | 9 | 10 | def new_request_id() -> str: 11 | """Generate a new request ID.""" 12 | return str(uuid.uuid4()) 13 | 14 | 15 | def set_request_id(request_id: str) -> None: 16 | """Set request ID in current context.""" 17 | _request_id_var.set(request_id) 18 | 19 | 20 | def get_request_id() -> str: 21 | """Get request ID from current context.""" 22 | return _request_id_var.get() 23 | 24 | 25 | def get_request_id_or_new() -> str: 26 | """Get request ID from context or generate a new one.""" 27 | request_id = get_request_id() 28 | if not request_id: 29 | request_id = new_request_id() 30 | set_request_id(request_id) 31 | return request_id 32 | -------------------------------------------------------------------------------- /go_src/internal/router/middleware_requestid.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/chatbotgang/workshop/internal/domain/common" 7 | ) 8 | 9 | const headerXRequestID = "X-Request-ID" 10 | 11 | // RequestIDMiddleware initializes the RequestID middleware. 12 | func RequestIDMiddleware() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | // Get id from request 15 | rid := c.GetHeader(headerXRequestID) 16 | if rid == "" { 17 | rid = common.NewRequestID() 18 | // Set the id to ensure that the requestid is in the request header 19 | c.Request.Header.Add(headerXRequestID, rid) 20 | } 21 | 22 | // Set the id to ensure that the requestid is in the request context 23 | ctx := common.SetRequestID(c.Request.Context(), rid) 24 | c.Request = c.Request.WithContext(ctx) 25 | 26 | // Set the id to ensure that the requestid is in the response 27 | c.Header(headerXRequestID, rid) 28 | c.Next() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/common/requestid.py: -------------------------------------------------------------------------------- 1 | """Request ID management utilities.""" 2 | 3 | import uuid 4 | from contextvars import ContextVar 5 | 6 | # Context variable to store request ID 7 | _request_id_var: ContextVar[str] = ContextVar("request_id", default="") 8 | 9 | 10 | def new_request_id() -> str: 11 | """Generate a new request ID.""" 12 | return str(uuid.uuid4()) 13 | 14 | 15 | def set_request_id(request_id: str) -> None: 16 | """Set request ID in current context.""" 17 | _request_id_var.set(request_id) 18 | 19 | 20 | def get_request_id() -> str: 21 | """Get request ID from current context.""" 22 | return _request_id_var.get() 23 | 24 | 25 | def get_request_id_or_new() -> str: 26 | """Get request ID from context or generate a new one.""" 27 | request_id = get_request_id() 28 | if not request_id: 29 | request_id = new_request_id() 30 | set_request_id(request_id) 31 | return request_id 32 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/common/requestid.py: -------------------------------------------------------------------------------- 1 | """Request ID management utilities.""" 2 | 3 | import uuid 4 | from contextvars import ContextVar 5 | 6 | # Context variable to store request ID 7 | _request_id_var: ContextVar[str] = ContextVar("request_id", default="") 8 | 9 | 10 | def new_request_id() -> str: 11 | """Generate a new request ID.""" 12 | return str(uuid.uuid4()) 13 | 14 | 15 | def set_request_id(request_id: str) -> None: 16 | """Set request ID in current context.""" 17 | _request_id_var.set(request_id) 18 | 19 | 20 | def get_request_id() -> str: 21 | """Get request ID from current context.""" 22 | return _request_id_var.get() 23 | 24 | 25 | def get_request_id_or_new() -> str: 26 | """Get request ID from context or generate a new one.""" 27 | request_id = get_request_id() 28 | if not request_id: 29 | request_id = new_request_id() 30 | set_request_id(request_id) 31 | return request_id 32 | -------------------------------------------------------------------------------- /python_src/internal/domain/common/error_code.py: -------------------------------------------------------------------------------- 1 | """Error code definitions for domain errors.""" 2 | 3 | from http import HTTPStatus 4 | 5 | from pydantic import BaseModel, ConfigDict 6 | 7 | 8 | class ErrorCode(BaseModel): 9 | """Defines an error code with name and HTTP status.""" 10 | 11 | model_config = ConfigDict(frozen=True) 12 | 13 | name: str 14 | status_code: int = HTTPStatus.INTERNAL_SERVER_ERROR 15 | 16 | 17 | # Common error codes 18 | UNKNOWN_ERROR = ErrorCode(name="UNKNOWN_ERROR", status_code=HTTPStatus.INTERNAL_SERVER_ERROR) 19 | VALIDATION_ERROR = ErrorCode(name="VALIDATION_ERROR", status_code=HTTPStatus.BAD_REQUEST) 20 | NOT_FOUND = ErrorCode(name="NOT_FOUND", status_code=HTTPStatus.NOT_FOUND) 21 | UNAUTHORIZED = ErrorCode(name="UNAUTHORIZED", status_code=HTTPStatus.UNAUTHORIZED) 22 | FORBIDDEN = ErrorCode(name="FORBIDDEN", status_code=HTTPStatus.FORBIDDEN) 23 | INTERNAL_ERROR = ErrorCode(name="INTERNAL_ERROR", status_code=HTTPStatus.INTERNAL_SERVER_ERROR) 24 | -------------------------------------------------------------------------------- /python_src/internal/domain/auto_reply/__init__.py: -------------------------------------------------------------------------------- 1 | """Auto Reply domain module.""" 2 | 3 | from internal.domain.auto_reply.auto_reply import AutoReply, AutoReplyEventType, AutoReplyStatus 4 | from internal.domain.auto_reply.webhook_trigger import ( 5 | BusinessHourSchedule, 6 | DailySchedule, 7 | DateRangeSchedule, 8 | MonthlySchedule, 9 | NonBusinessHourSchedule, 10 | WebhookTriggerEventType, 11 | WebhookTriggerSchedule, 12 | WebhookTriggerScheduleSettings, 13 | WebhookTriggerScheduleType, 14 | WebhookTriggerSetting, 15 | ) 16 | 17 | __all__ = [ 18 | "AutoReply", 19 | "AutoReplyStatus", 20 | "AutoReplyEventType", 21 | "WebhookTriggerSetting", 22 | "WebhookTriggerEventType", 23 | "WebhookTriggerScheduleType", 24 | "WebhookTriggerScheduleSettings", 25 | "WebhookTriggerSchedule", 26 | "DailySchedule", 27 | "MonthlySchedule", 28 | "DateRangeSchedule", 29 | "BusinessHourSchedule", 30 | "NonBusinessHourSchedule", 31 | ] 32 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/common/error_code.py: -------------------------------------------------------------------------------- 1 | """Error code definitions for domain errors.""" 2 | 3 | from http import HTTPStatus 4 | 5 | from pydantic import BaseModel, ConfigDict 6 | 7 | 8 | class ErrorCode(BaseModel): 9 | """Defines an error code with name and HTTP status.""" 10 | 11 | model_config = ConfigDict(frozen=True) 12 | 13 | name: str 14 | status_code: int = HTTPStatus.INTERNAL_SERVER_ERROR 15 | 16 | 17 | # Common error codes 18 | UNKNOWN_ERROR = ErrorCode(name="UNKNOWN_ERROR", status_code=HTTPStatus.INTERNAL_SERVER_ERROR) 19 | VALIDATION_ERROR = ErrorCode(name="VALIDATION_ERROR", status_code=HTTPStatus.BAD_REQUEST) 20 | NOT_FOUND = ErrorCode(name="NOT_FOUND", status_code=HTTPStatus.NOT_FOUND) 21 | UNAUTHORIZED = ErrorCode(name="UNAUTHORIZED", status_code=HTTPStatus.UNAUTHORIZED) 22 | FORBIDDEN = ErrorCode(name="FORBIDDEN", status_code=HTTPStatus.FORBIDDEN) 23 | INTERNAL_ERROR = ErrorCode(name="INTERNAL_ERROR", status_code=HTTPStatus.INTERNAL_SERVER_ERROR) 24 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/common/error_code.py: -------------------------------------------------------------------------------- 1 | """Error code definitions for domain errors.""" 2 | 3 | from http import HTTPStatus 4 | 5 | from pydantic import BaseModel, ConfigDict 6 | 7 | 8 | class ErrorCode(BaseModel): 9 | """Defines an error code with name and HTTP status.""" 10 | 11 | model_config = ConfigDict(frozen=True) 12 | 13 | name: str 14 | status_code: int = HTTPStatus.INTERNAL_SERVER_ERROR 15 | 16 | 17 | # Common error codes 18 | UNKNOWN_ERROR = ErrorCode(name="UNKNOWN_ERROR", status_code=HTTPStatus.INTERNAL_SERVER_ERROR) 19 | VALIDATION_ERROR = ErrorCode(name="VALIDATION_ERROR", status_code=HTTPStatus.BAD_REQUEST) 20 | NOT_FOUND = ErrorCode(name="NOT_FOUND", status_code=HTTPStatus.NOT_FOUND) 21 | UNAUTHORIZED = ErrorCode(name="UNAUTHORIZED", status_code=HTTPStatus.UNAUTHORIZED) 22 | FORBIDDEN = ErrorCode(name="FORBIDDEN", status_code=HTTPStatus.FORBIDDEN) 23 | INTERNAL_ERROR = ErrorCode(name="INTERNAL_ERROR", status_code=HTTPStatus.INTERNAL_SERVER_ERROR) 24 | -------------------------------------------------------------------------------- /python_src/internal/domain/organization/organization.py: -------------------------------------------------------------------------------- 1 | """Organization domain models.""" 2 | 3 | from datetime import datetime 4 | from enum import StrEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class LanguageCode(StrEnum): 10 | """Language code enumeration.""" 11 | 12 | ZH_HANT = "zh-hant" 13 | ZH_HANS = "zh-hans" 14 | EN = "en" 15 | 16 | 17 | class Organization(BaseModel): 18 | """Organization domain model. 19 | 20 | Represents an organization entity in the system. 21 | It contains the core business information and settings for an organization. 22 | """ 23 | 24 | id: int 25 | name: str 26 | uuid: str 27 | plan_id: str | None = None 28 | enable_two_factor: bool 29 | timezone: str 30 | language_code: LanguageCode 31 | enable: bool 32 | created_at: datetime 33 | updated_at: datetime 34 | expired_at: datetime | None = None 35 | 36 | class Config: 37 | """Pydantic configuration.""" 38 | 39 | use_enum_values = True 40 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/organization/organization.py: -------------------------------------------------------------------------------- 1 | """Organization domain models.""" 2 | 3 | from datetime import datetime 4 | from enum import StrEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class LanguageCode(StrEnum): 10 | """Language code enumeration.""" 11 | 12 | ZH_HANT = "zh-hant" 13 | ZH_HANS = "zh-hans" 14 | EN = "en" 15 | 16 | 17 | class Organization(BaseModel): 18 | """Organization domain model. 19 | 20 | Represents an organization entity in the system. 21 | It contains the core business information and settings for an organization. 22 | """ 23 | 24 | id: int 25 | name: str 26 | uuid: str 27 | plan_id: str | None = None 28 | enable_two_factor: bool 29 | timezone: str 30 | language_code: LanguageCode 31 | enable: bool 32 | created_at: datetime 33 | updated_at: datetime 34 | expired_at: datetime | None = None 35 | 36 | class Config: 37 | """Pydantic configuration.""" 38 | 39 | use_enum_values = True 40 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/organization/organization.py: -------------------------------------------------------------------------------- 1 | """Organization domain models.""" 2 | 3 | from datetime import datetime 4 | from enum import StrEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class LanguageCode(StrEnum): 10 | """Language code enumeration.""" 11 | 12 | ZH_HANT = "zh-hant" 13 | ZH_HANS = "zh-hans" 14 | EN = "en" 15 | 16 | 17 | class Organization(BaseModel): 18 | """Organization domain model. 19 | 20 | Represents an organization entity in the system. 21 | It contains the core business information and settings for an organization. 22 | """ 23 | 24 | id: int 25 | name: str 26 | uuid: str 27 | plan_id: str | None = None 28 | enable_two_factor: bool 29 | timezone: str 30 | language_code: LanguageCode 31 | enable: bool 32 | created_at: datetime 33 | updated_at: datetime 34 | expired_at: datetime | None = None 35 | 36 | class Config: 37 | """Pydantic configuration.""" 38 | 39 | use_enum_values = True 40 | -------------------------------------------------------------------------------- /cheat_sheet/go/2_extend_feature/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 8 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /cheat_sheet/go/1_rewrite_brownfield/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 8 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /legacy/line/tasks.py: -------------------------------------------------------------------------------- 1 | from celery.utils.log import get_task_logger 2 | 3 | from line.repositories import ( 4 | webhook_repository, 5 | ) 6 | from organization.repositories import organization_repository 7 | from rubato.celery import ( 8 | QUEUE_LINE_WEBHOOK, 9 | app, 10 | ) 11 | from line.services.webhook import ProcessLineWebhookService 12 | 13 | 14 | logger = get_task_logger("rubato_celery") 15 | 16 | process_line_webhook_service = ProcessLineWebhookService( 17 | organization_repository=organization_repository, 18 | webhook_repository=webhook_repository, 19 | ) 20 | 21 | 22 | @app.task(bind=True, queue=QUEUE_LINE_WEBHOOK) 23 | def webhook_event_handler(self, *, bot: dict, event: dict): 24 | try: 25 | process_line_webhook_service.execute(bot_dict=bot, event_data=event) 26 | except Exception as exc: 27 | logger.exception( 28 | { 29 | "message": "[webhook_event_handler] LINE webhook event processing failed", 30 | "bot_id": bot["id"], 31 | "event": event, 32 | "exc": exc, 33 | } 34 | ) 35 | return f"[webhook_event_handler] {bot}|{event}" 36 | -------------------------------------------------------------------------------- /go_src/internal/domain/organization/organization.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // LanguageCode represents supported language codes. 8 | type LanguageCode string 9 | 10 | const ( 11 | LanguageCodeZhHant LanguageCode = "zh-hant" 12 | LanguageCodeZhHans LanguageCode = "zh-hans" 13 | LanguageCodeEn LanguageCode = "en" 14 | // Add more language codes as needed 15 | ) 16 | 17 | // Organization represents an organization entity in the system. 18 | // It contains the core business information and settings for an organization. 19 | type Organization struct { 20 | ID int `json:"id"` 21 | Name string `json:"name"` 22 | UUID string `json:"uuid"` 23 | PlanID *string `json:"plan_id,omitempty"` 24 | EnableTwoFactor bool `json:"enable_two_factor"` 25 | Timezone string `json:"timezone"` 26 | LanguageCode LanguageCode `json:"language_code"` 27 | Enable bool `json:"enable"` 28 | CreatedAt time.Time `json:"created_at"` 29 | UpdatedAt time.Time `json:"updated_at"` 30 | ExpiredAt *time.Time `json:"expired_at,omitempty"` 31 | } 32 | -------------------------------------------------------------------------------- /cheat_sheet/go/2_extend_feature/internal/domain/organization/organization.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // LanguageCode represents supported language codes. 8 | type LanguageCode string 9 | 10 | const ( 11 | LanguageCodeZhHant LanguageCode = "zh-hant" 12 | LanguageCodeZhHans LanguageCode = "zh-hans" 13 | LanguageCodeEn LanguageCode = "en" 14 | // Add more language codes as needed 15 | ) 16 | 17 | // Organization represents an organization entity in the system. 18 | // It contains the core business information and settings for an organization. 19 | type Organization struct { 20 | ID int `json:"id"` 21 | Name string `json:"name"` 22 | UUID string `json:"uuid"` 23 | PlanID *string `json:"plan_id,omitempty"` 24 | EnableTwoFactor bool `json:"enable_two_factor"` 25 | Timezone string `json:"timezone"` 26 | LanguageCode LanguageCode `json:"language_code"` 27 | Enable bool `json:"enable"` 28 | CreatedAt time.Time `json:"created_at"` 29 | UpdatedAt time.Time `json:"updated_at"` 30 | ExpiredAt *time.Time `json:"expired_at,omitempty"` 31 | } 32 | -------------------------------------------------------------------------------- /cheat_sheet/go/1_rewrite_brownfield/internal/domain/organization/organization.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // LanguageCode represents supported language codes. 8 | type LanguageCode string 9 | 10 | const ( 11 | LanguageCodeZhHant LanguageCode = "zh-hant" 12 | LanguageCodeZhHans LanguageCode = "zh-hans" 13 | LanguageCodeEn LanguageCode = "en" 14 | // Add more language codes as needed 15 | ) 16 | 17 | // Organization represents an organization entity in the system. 18 | // It contains the core business information and settings for an organization. 19 | type Organization struct { 20 | ID int `json:"id"` 21 | Name string `json:"name"` 22 | UUID string `json:"uuid"` 23 | PlanID *string `json:"plan_id,omitempty"` 24 | EnableTwoFactor bool `json:"enable_two_factor"` 25 | Timezone string `json:"timezone"` 26 | LanguageCode LanguageCode `json:"language_code"` 27 | Enable bool `json:"enable"` 28 | CreatedAt time.Time `json:"created_at"` 29 | UpdatedAt time.Time `json:"updated_at"` 30 | ExpiredAt *time.Time `json:"expired_at,omitempty"` 31 | } 32 | -------------------------------------------------------------------------------- /python_src/internal/router/handlers.py: -------------------------------------------------------------------------------- 1 | """HTTP route handlers for the application.""" 2 | 3 | from fastapi import APIRouter, status 4 | from fastapi.responses import JSONResponse 5 | 6 | from entrypoint.app.settings import AppConfig 7 | from internal.domain.common.requestid import get_request_id_or_new 8 | 9 | # Global config instance for health check 10 | _config = AppConfig() 11 | 12 | 13 | def create_api_router() -> APIRouter: 14 | """Create and configure the API router with all endpoints.""" 15 | router = APIRouter(prefix="/api") 16 | 17 | # Create v1 router 18 | v1_router = APIRouter(prefix="/v1") 19 | 20 | # Add health check endpoint 21 | v1_router.add_api_route( 22 | "/health", health_check, methods=["GET"], status_code=status.HTTP_200_OK, response_class=JSONResponse 23 | ) 24 | 25 | # Include v1 router in main router 26 | router.include_router(v1_router) 27 | 28 | return router 29 | 30 | 31 | async def health_check() -> JSONResponse: 32 | """Health check endpoint.""" 33 | request_id = get_request_id_or_new() 34 | 35 | response_data = {"status": "healthy", "env": _config.env, "request_id": request_id} 36 | 37 | return JSONResponse(content=response_data, status_code=status.HTTP_200_OK) 38 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/router/handlers.py: -------------------------------------------------------------------------------- 1 | """HTTP route handlers for the application.""" 2 | 3 | from fastapi import APIRouter, status 4 | from fastapi.responses import JSONResponse 5 | 6 | from entrypoint.app.settings import AppConfig 7 | from internal.domain.common.requestid import get_request_id_or_new 8 | 9 | # Global config instance for health check 10 | _config = AppConfig() 11 | 12 | 13 | def create_api_router() -> APIRouter: 14 | """Create and configure the API router with all endpoints.""" 15 | router = APIRouter(prefix="/api") 16 | 17 | # Create v1 router 18 | v1_router = APIRouter(prefix="/v1") 19 | 20 | # Add health check endpoint 21 | v1_router.add_api_route( 22 | "/health", health_check, methods=["GET"], status_code=status.HTTP_200_OK, response_class=JSONResponse 23 | ) 24 | 25 | # Include v1 router in main router 26 | router.include_router(v1_router) 27 | 28 | return router 29 | 30 | 31 | async def health_check() -> JSONResponse: 32 | """Health check endpoint.""" 33 | request_id = get_request_id_or_new() 34 | 35 | response_data = {"status": "healthy", "env": _config.env, "request_id": request_id} 36 | 37 | return JSONResponse(content=response_data, status_code=status.HTTP_200_OK) 38 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/router/handlers.py: -------------------------------------------------------------------------------- 1 | """HTTP route handlers for the application.""" 2 | 3 | from fastapi import APIRouter, status 4 | from fastapi.responses import JSONResponse 5 | 6 | from entrypoint.app.settings import AppConfig 7 | from internal.domain.common.requestid import get_request_id_or_new 8 | 9 | # Global config instance for health check 10 | _config = AppConfig() 11 | 12 | 13 | def create_api_router() -> APIRouter: 14 | """Create and configure the API router with all endpoints.""" 15 | router = APIRouter(prefix="/api") 16 | 17 | # Create v1 router 18 | v1_router = APIRouter(prefix="/v1") 19 | 20 | # Add health check endpoint 21 | v1_router.add_api_route( 22 | "/health", health_check, methods=["GET"], status_code=status.HTTP_200_OK, response_class=JSONResponse 23 | ) 24 | 25 | # Include v1 router in main router 26 | router.include_router(v1_router) 27 | 28 | return router 29 | 30 | 31 | async def health_check() -> JSONResponse: 32 | """Health check endpoint.""" 33 | request_id = get_request_id_or_new() 34 | 35 | response_data = {"status": "healthy", "env": _config.env, "request_id": request_id} 36 | 37 | return JSONResponse(content=response_data, status_code=status.HTTP_200_OK) 38 | -------------------------------------------------------------------------------- /go_src/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | GIT_BRANCH=$(shell git branch | grep \* | cut -d ' ' -f2) 4 | GIT_REV=$(shell git rev-parse HEAD | cut -c1-7) 5 | BUILD_DATE=$(shell date +%Y-%m-%d.%H:%M:%S) 6 | EXTRA_LD_FLAGS=-X main.AppVersion=${GIT_BRANCH}-${GIT_REV} -X main.AppBuild=${BUILD_DATE} 7 | 8 | GOLANGCI_LINT_VERSION="v2.2.1" 9 | 10 | # Setup test packages 11 | TEST_PACKAGES = ./internal/... 12 | 13 | clean: 14 | rm -rf bin/ cover.out 15 | 16 | test: 17 | go test -count=1 -race $(TEST_PACKAGES) 18 | 19 | test-verbose: 20 | go test -v -count=1 -race -cover -coverprofile cover.out $(TEST_PACKAGES) 21 | go tool cover -func=cover.out | tail -n 1 22 | 23 | lint: 24 | @if [ ! -f ./bin/golangci-lint ]; then \ 25 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s $(GOLANGCI_LINT_VERSION); \ 26 | fi; 27 | @echo "golangci-lint checking..." 28 | @./bin/golangci-lint -v run $(TEST_PACKAGES) ./cmd/... 29 | 30 | mock: 31 | @which mockgen > /dev/null || (echo "No mockgen installed. Try: go install github.com/golang/mock/mockgen@v1.6.0"; exit 1) 32 | @which gowrap > /dev/null || (echo "No gowrap installed. Try: go install github.com/hexdigest/gowrap/cmd/gowrap@v1.2.7"; exit 1) 33 | @echo "generating mocks..." 34 | @go generate ./... 35 | 36 | build: 37 | go build -ldflags '${EXTRA_LD_FLAGS}' -o bin/app ./cmd/app 38 | 39 | help: build 40 | ./bin/app --help 41 | 42 | run: build 43 | ./bin/app \ 44 | --env="staging" \ 45 | --log_level="debug" \ 46 | --port=8080 47 | -------------------------------------------------------------------------------- /python_src/internal/domain/auto_reply/auto_reply.py: -------------------------------------------------------------------------------- 1 | """Auto Reply domain models.""" 2 | 3 | from datetime import datetime 4 | from enum import StrEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | from internal.domain.auto_reply.webhook_trigger import WebhookTriggerScheduleSettings, WebhookTriggerScheduleType 9 | 10 | 11 | class AutoReplyStatus(StrEnum): 12 | """Auto reply status enumeration.""" 13 | 14 | ACTIVE = "active" 15 | INACTIVE = "inactive" 16 | ARCHIVED = "archived" 17 | 18 | 19 | class AutoReplyEventType(StrEnum): 20 | """Auto reply event type enumeration.""" 21 | 22 | MESSAGE = "message" 23 | POSTBACK = "postback" 24 | FOLLOW = "follow" 25 | BEACON = "beacon" 26 | TIME = "time" 27 | KEYWORD = "keyword" 28 | DEFAULT = "default" 29 | 30 | 31 | class AutoReply(BaseModel): 32 | """Auto reply domain model. 33 | 34 | Represents an omnichannel rule that associates several WebhookTriggerSetting instances. 35 | It defines the high-level auto-reply configuration for an organization. 36 | """ 37 | 38 | id: int 39 | organization_id: int 40 | name: str 41 | status: AutoReplyStatus 42 | event_type: AutoReplyEventType 43 | priority: int 44 | keywords: list[str] | None = None 45 | trigger_schedule_type: WebhookTriggerScheduleType | None = None 46 | trigger_schedule_settings: WebhookTriggerScheduleSettings | None = None 47 | created_at: datetime 48 | updated_at: datetime 49 | 50 | class Config: 51 | """Pydantic configuration.""" 52 | 53 | use_enum_values = True 54 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/auto_reply/auto_reply.py: -------------------------------------------------------------------------------- 1 | """Auto Reply domain models.""" 2 | 3 | from datetime import datetime 4 | from enum import StrEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | from internal.domain.auto_reply.webhook_trigger import WebhookTriggerScheduleSettings, WebhookTriggerScheduleType 9 | 10 | 11 | class AutoReplyStatus(StrEnum): 12 | """Auto reply status enumeration.""" 13 | 14 | ACTIVE = "active" 15 | INACTIVE = "inactive" 16 | ARCHIVED = "archived" 17 | 18 | 19 | class AutoReplyEventType(StrEnum): 20 | """Auto reply event type enumeration.""" 21 | 22 | MESSAGE = "message" 23 | POSTBACK = "postback" 24 | FOLLOW = "follow" 25 | BEACON = "beacon" 26 | TIME = "time" 27 | KEYWORD = "keyword" 28 | DEFAULT = "default" 29 | 30 | 31 | class AutoReply(BaseModel): 32 | """Auto reply domain model. 33 | 34 | Represents an omnichannel rule that associates several WebhookTriggerSetting instances. 35 | It defines the high-level auto-reply configuration for an organization. 36 | """ 37 | 38 | id: int 39 | organization_id: int 40 | name: str 41 | status: AutoReplyStatus 42 | event_type: AutoReplyEventType 43 | priority: int 44 | keywords: list[str] | None = None 45 | trigger_schedule_type: WebhookTriggerScheduleType | None = None 46 | trigger_schedule_settings: WebhookTriggerScheduleSettings | None = None 47 | created_at: datetime 48 | updated_at: datetime 49 | 50 | class Config: 51 | """Pydantic configuration.""" 52 | 53 | use_enum_values = True 54 | -------------------------------------------------------------------------------- /go_src/internal/router/response.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | type ErrorCategory string 4 | 5 | type ErrorMessage struct { 6 | Name string `json:"name"` 7 | Code int `json:"code"` 8 | ResolveByRetry bool `json:"resolve_by_retry"` 9 | Message string `json:"message"` 10 | RemoteCode int `json:"remoteCode,omitempty"` 11 | Detail map[string]interface{} `json:"detail,omitempty"` 12 | } 13 | 14 | // func respondWithJSON(c *gin.Context, code int, payload interface{}) { 15 | // c.JSON(code, payload) 16 | // } 17 | 18 | // func respondWithoutBody(c *gin.Context, code int) { 19 | // c.Status(code) 20 | // } 21 | 22 | // func respondWithError(c *gin.Context, err error) { 23 | // errMessage := parseError(err) 24 | 25 | // ctx := c.Request.Context() 26 | // zerolog.Ctx(ctx).Error().Err(err).Str("component", "handler").Msg(errMessage.Message) 27 | // _ = c.Error(err) 28 | // c.AbortWithStatusJSON(errMessage.Code, errMessage) 29 | // } 30 | 31 | // func parseError(err error) ErrorMessage { 32 | // var domainError common.DomainError 33 | // // We don't check if errors.As is valid or not 34 | // // because an empty common.DomainError would return default error data. 35 | // _ = errors.As(err, &domainError) 36 | 37 | // return ErrorMessage{ 38 | // Name: domainError.Name(), 39 | // Code: domainError.HTTPStatus(), 40 | // Message: domainError.ClientMsg(), 41 | // RemoteCode: domainError.RemoteHTTPStatus(), 42 | // Detail: domainError.Detail(), 43 | // } 44 | // } 45 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/auto_reply/auto_reply.py: -------------------------------------------------------------------------------- 1 | """Auto Reply domain models.""" 2 | 3 | from datetime import datetime 4 | from enum import StrEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | from internal.domain.auto_reply.webhook_trigger import WebhookTriggerScheduleSettings, WebhookTriggerScheduleType 9 | 10 | 11 | class AutoReplyStatus(StrEnum): 12 | """Auto reply status enumeration.""" 13 | 14 | ACTIVE = "active" 15 | INACTIVE = "inactive" 16 | ARCHIVED = "archived" 17 | 18 | 19 | class AutoReplyEventType(StrEnum): 20 | """Auto reply event type enumeration.""" 21 | 22 | MESSAGE = "message" 23 | POSTBACK = "postback" 24 | FOLLOW = "follow" 25 | BEACON = "beacon" 26 | TIME = "time" 27 | KEYWORD = "keyword" 28 | DEFAULT = "default" 29 | 30 | 31 | class AutoReply(BaseModel): 32 | """Auto reply domain model. 33 | 34 | Represents an omnichannel rule that associates several WebhookTriggerSetting instances. 35 | It defines the high-level auto-reply configuration for an organization. 36 | """ 37 | 38 | id: int 39 | organization_id: int 40 | name: str 41 | status: AutoReplyStatus 42 | event_type: AutoReplyEventType 43 | priority: int 44 | keywords: list[str] | None = None 45 | trigger_schedule_type: WebhookTriggerScheduleType | None = None 46 | trigger_schedule_settings: WebhookTriggerScheduleSettings | None = None 47 | created_at: datetime 48 | updated_at: datetime 49 | 50 | class Config: 51 | """Pydantic configuration.""" 52 | 53 | use_enum_values = True 54 | -------------------------------------------------------------------------------- /python_src/internal/domain/organization/business_hour.py: -------------------------------------------------------------------------------- 1 | """Business Hour domain models.""" 2 | 3 | from datetime import time 4 | from enum import IntEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class WeekDay(IntEnum): 10 | """Week day enumeration (Monday = 0, Sunday = 6).""" 11 | 12 | MONDAY = 0 13 | TUESDAY = 1 14 | WEDNESDAY = 2 15 | THURSDAY = 3 16 | FRIDAY = 4 17 | SATURDAY = 5 18 | SUNDAY = 6 19 | 20 | 21 | class BusinessHour(BaseModel): 22 | """Business hour configuration for an organization.""" 23 | 24 | id: int 25 | organization_id: int 26 | day_of_week: WeekDay 27 | start_time: time 28 | end_time: time 29 | is_active: bool = True 30 | 31 | def is_day_active(self, weekday: int) -> bool: 32 | """Check if business hours are active for the given weekday. 33 | 34 | Args: 35 | weekday: Weekday number (Monday = 0, Sunday = 6) 36 | 37 | Returns: 38 | True if business hours are active for this day, False otherwise 39 | """ 40 | return self.is_active and self.day_of_week == weekday 41 | 42 | def is_time_within_hours(self, check_time: time) -> bool: 43 | """Check if the given time is within business hours. 44 | 45 | Args: 46 | check_time: Time to check 47 | 48 | Returns: 49 | True if time is within business hours, False otherwise 50 | """ 51 | return self.start_time <= check_time <= self.end_time 52 | 53 | class Config: 54 | """Pydantic configuration.""" 55 | 56 | use_enum_values = True 57 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/organization/business_hour.py: -------------------------------------------------------------------------------- 1 | """Business Hour domain models.""" 2 | 3 | from datetime import time 4 | from enum import IntEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class WeekDay(IntEnum): 10 | """Week day enumeration (Monday = 0, Sunday = 6).""" 11 | 12 | MONDAY = 0 13 | TUESDAY = 1 14 | WEDNESDAY = 2 15 | THURSDAY = 3 16 | FRIDAY = 4 17 | SATURDAY = 5 18 | SUNDAY = 6 19 | 20 | 21 | class BusinessHour(BaseModel): 22 | """Business hour configuration for an organization.""" 23 | 24 | id: int 25 | organization_id: int 26 | day_of_week: WeekDay 27 | start_time: time 28 | end_time: time 29 | is_active: bool = True 30 | 31 | def is_day_active(self, weekday: int) -> bool: 32 | """Check if business hours are active for the given weekday. 33 | 34 | Args: 35 | weekday: Weekday number (Monday = 0, Sunday = 6) 36 | 37 | Returns: 38 | True if business hours are active for this day, False otherwise 39 | """ 40 | return self.is_active and self.day_of_week == weekday 41 | 42 | def is_time_within_hours(self, check_time: time) -> bool: 43 | """Check if the given time is within business hours. 44 | 45 | Args: 46 | check_time: Time to check 47 | 48 | Returns: 49 | True if time is within business hours, False otherwise 50 | """ 51 | return self.start_time <= check_time <= self.end_time 52 | 53 | class Config: 54 | """Pydantic configuration.""" 55 | 56 | use_enum_values = True 57 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/organization/business_hour.py: -------------------------------------------------------------------------------- 1 | """Business Hour domain models.""" 2 | 3 | from datetime import time 4 | from enum import IntEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class WeekDay(IntEnum): 10 | """Week day enumeration (Monday = 0, Sunday = 6).""" 11 | 12 | MONDAY = 0 13 | TUESDAY = 1 14 | WEDNESDAY = 2 15 | THURSDAY = 3 16 | FRIDAY = 4 17 | SATURDAY = 5 18 | SUNDAY = 6 19 | 20 | 21 | class BusinessHour(BaseModel): 22 | """Business hour configuration for an organization.""" 23 | 24 | id: int 25 | organization_id: int 26 | day_of_week: WeekDay 27 | start_time: time 28 | end_time: time 29 | is_active: bool = True 30 | 31 | def is_day_active(self, weekday: int) -> bool: 32 | """Check if business hours are active for the given weekday. 33 | 34 | Args: 35 | weekday: Weekday number (Monday = 0, Sunday = 6) 36 | 37 | Returns: 38 | True if business hours are active for this day, False otherwise 39 | """ 40 | return self.is_active and self.day_of_week == weekday 41 | 42 | def is_time_within_hours(self, check_time: time) -> bool: 43 | """Check if the given time is within business hours. 44 | 45 | Args: 46 | check_time: Time to check 47 | 48 | Returns: 49 | True if time is within business hours, False otherwise 50 | """ 51 | return self.start_time <= check_time <= self.end_time 52 | 53 | class Config: 54 | """Pydantic configuration.""" 55 | 56 | use_enum_values = True 57 | -------------------------------------------------------------------------------- /go_src/internal/router/middleware_zwsp_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestRemoveZWSPMiddleware(t *testing.T) { 15 | t.Parallel() 16 | 17 | // Test cases 18 | testCases := []struct { 19 | Name string 20 | Test string 21 | Expect string 22 | }{ 23 | { 24 | Name: "has-zwsp", 25 | Test: "HERE>\u200B\u200C\u200D\uFEFF\u0000\\u0000 bool: 38 | """Check if the bot is enabled and not expired.""" 39 | if not self.enable: 40 | return False 41 | if self.expired_at is not None and datetime.now() > self.expired_at: 42 | return False 43 | return True 44 | 45 | def is_token_expired(self) -> bool: 46 | """Check if the bot's access token has expired.""" 47 | return self.token_expired_time < datetime.now() 48 | 49 | def is_token_valid(self) -> bool: 50 | """Check if the bot has a valid access token.""" 51 | return self.access_token != "" and not self.is_token_expired() 52 | 53 | class Config: 54 | """Pydantic configuration.""" 55 | 56 | use_enum_values = True 57 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/organization/bot.py: -------------------------------------------------------------------------------- 1 | """Bot domain models.""" 2 | 3 | from datetime import datetime 4 | from enum import StrEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class BotType(StrEnum): 10 | """Bot type enumeration.""" 11 | 12 | LINE = "LINE" 13 | FACEBOOK = "FB" 14 | INSTAGRAM = "IG" 15 | 16 | 17 | class Bot(BaseModel): 18 | """Bot domain model. 19 | 20 | Represents a channel entity associated with an organization. 21 | It contains the configuration and credentials for integration across multiple platforms. 22 | """ 23 | 24 | id: int 25 | organization_id: int 26 | name: str 27 | type: BotType 28 | channel_id: str 29 | channel_secret: str 30 | access_token: str 31 | token_expired_time: datetime 32 | created_at: datetime 33 | updated_at: datetime 34 | expired_at: datetime | None = None 35 | enable: bool 36 | 37 | def is_active(self) -> bool: 38 | """Check if the bot is enabled and not expired.""" 39 | if not self.enable: 40 | return False 41 | if self.expired_at is not None and datetime.now() > self.expired_at: 42 | return False 43 | return True 44 | 45 | def is_token_expired(self) -> bool: 46 | """Check if the bot's access token has expired.""" 47 | return self.token_expired_time < datetime.now() 48 | 49 | def is_token_valid(self) -> bool: 50 | """Check if the bot has a valid access token.""" 51 | return self.access_token != "" and not self.is_token_expired() 52 | 53 | class Config: 54 | """Pydantic configuration.""" 55 | 56 | use_enum_values = True 57 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/organization/bot.py: -------------------------------------------------------------------------------- 1 | """Bot domain models.""" 2 | 3 | from datetime import datetime 4 | from enum import StrEnum 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class BotType(StrEnum): 10 | """Bot type enumeration.""" 11 | 12 | LINE = "LINE" 13 | FACEBOOK = "FB" 14 | INSTAGRAM = "IG" 15 | 16 | 17 | class Bot(BaseModel): 18 | """Bot domain model. 19 | 20 | Represents a channel entity associated with an organization. 21 | It contains the configuration and credentials for integration across multiple platforms. 22 | """ 23 | 24 | id: int 25 | organization_id: int 26 | name: str 27 | type: BotType 28 | channel_id: str 29 | channel_secret: str 30 | access_token: str 31 | token_expired_time: datetime 32 | created_at: datetime 33 | updated_at: datetime 34 | expired_at: datetime | None = None 35 | enable: bool 36 | 37 | def is_active(self) -> bool: 38 | """Check if the bot is enabled and not expired.""" 39 | if not self.enable: 40 | return False 41 | if self.expired_at is not None and datetime.now() > self.expired_at: 42 | return False 43 | return True 44 | 45 | def is_token_expired(self) -> bool: 46 | """Check if the bot's access token has expired.""" 47 | return self.token_expired_time < datetime.now() 48 | 49 | def is_token_valid(self) -> bool: 50 | """Check if the bot has a valid access token.""" 51 | return self.access_token != "" and not self.is_token_expired() 52 | 53 | class Config: 54 | """Pydantic configuration.""" 55 | 56 | use_enum_values = True 57 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/auto_reply/__init__.py: -------------------------------------------------------------------------------- 1 | """Auto Reply domain module.""" 2 | 3 | from internal.domain.auto_reply.auto_reply import AutoReply, AutoReplyEventType, AutoReplyStatus 4 | from internal.domain.auto_reply.trigger_validation import ( 5 | BusinessHourChecker, 6 | TriggerValidationResult, 7 | convert_to_timezone, 8 | validate_trigger, 9 | ) 10 | from internal.domain.auto_reply.webhook_event import ( 11 | BeaconEvent, 12 | ChannelType, 13 | FollowEvent, 14 | MessageEvent, 15 | PostbackEvent, 16 | WebhookEvent, 17 | WebhookEventType, 18 | ) 19 | from internal.domain.auto_reply.webhook_trigger import ( 20 | BusinessHourSchedule, 21 | DailySchedule, 22 | DateRangeSchedule, 23 | MonthlySchedule, 24 | NonBusinessHourSchedule, 25 | WebhookTriggerEventType, 26 | WebhookTriggerSchedule, 27 | WebhookTriggerScheduleSettings, 28 | WebhookTriggerScheduleType, 29 | WebhookTriggerSetting, 30 | ) 31 | 32 | __all__ = [ 33 | "AutoReply", 34 | "AutoReplyStatus", 35 | "AutoReplyEventType", 36 | "WebhookTriggerSetting", 37 | "WebhookTriggerEventType", 38 | "WebhookTriggerScheduleType", 39 | "WebhookTriggerScheduleSettings", 40 | "WebhookTriggerSchedule", 41 | "DailySchedule", 42 | "MonthlySchedule", 43 | "DateRangeSchedule", 44 | "BusinessHourSchedule", 45 | "NonBusinessHourSchedule", 46 | "WebhookEvent", 47 | "WebhookEventType", 48 | "MessageEvent", 49 | "PostbackEvent", 50 | "FollowEvent", 51 | "BeaconEvent", 52 | "ChannelType", 53 | "validate_trigger", 54 | "TriggerValidationResult", 55 | "BusinessHourChecker", 56 | "convert_to_timezone", 57 | ] 58 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/auto_reply/__init__.py: -------------------------------------------------------------------------------- 1 | """Auto Reply domain module.""" 2 | 3 | from internal.domain.auto_reply.auto_reply import AutoReply, AutoReplyEventType, AutoReplyStatus 4 | from internal.domain.auto_reply.trigger_validation import ( 5 | BusinessHourChecker, 6 | TriggerValidationResult, 7 | convert_to_timezone, 8 | validate_trigger, 9 | ) 10 | from internal.domain.auto_reply.webhook_event import ( 11 | BeaconEvent, 12 | ChannelType, 13 | FollowEvent, 14 | MessageEvent, 15 | PostbackEvent, 16 | WebhookEvent, 17 | WebhookEventType, 18 | ) 19 | from internal.domain.auto_reply.webhook_trigger import ( 20 | BusinessHourSchedule, 21 | DailySchedule, 22 | DateRangeSchedule, 23 | MonthlySchedule, 24 | NonBusinessHourSchedule, 25 | WebhookTriggerEventType, 26 | WebhookTriggerSchedule, 27 | WebhookTriggerScheduleSettings, 28 | WebhookTriggerScheduleType, 29 | WebhookTriggerSetting, 30 | ) 31 | 32 | __all__ = [ 33 | "AutoReply", 34 | "AutoReplyStatus", 35 | "AutoReplyEventType", 36 | "WebhookTriggerSetting", 37 | "WebhookTriggerEventType", 38 | "WebhookTriggerScheduleType", 39 | "WebhookTriggerScheduleSettings", 40 | "WebhookTriggerSchedule", 41 | "DailySchedule", 42 | "MonthlySchedule", 43 | "DateRangeSchedule", 44 | "BusinessHourSchedule", 45 | "NonBusinessHourSchedule", 46 | "WebhookEvent", 47 | "WebhookEventType", 48 | "MessageEvent", 49 | "PostbackEvent", 50 | "FollowEvent", 51 | "BeaconEvent", 52 | "ChannelType", 53 | "validate_trigger", 54 | "TriggerValidationResult", 55 | "BusinessHourChecker", 56 | "convert_to_timezone", 57 | ] 58 | -------------------------------------------------------------------------------- /python_src/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ai-coding-workshop-250712" 3 | version = "0.1.0" 4 | description = "" 5 | requires-python = ">=3.12" 6 | dependencies = [ 7 | "pydantic (>=2.11.7,<3.0.0)", 8 | "pytz (>=2025.2,<2026.0)", 9 | "pydantic-settings (>=2.10.1,<3.0.0)", 10 | "httpx (>=0.28.1,<0.29.0)", 11 | "uvicorn[standard] (>=0.35.0,<0.36.0)", 12 | "structlog (>=25.4.0,<26.0.0)", 13 | "fastapi (>=0.115.14,<0.116.0)" 14 | ] 15 | 16 | 17 | [build-system] 18 | requires = ["poetry-core>=2.0.0,<3.0.0"] 19 | build-backend = "poetry.core.masonry.api" 20 | 21 | [tool.poetry] 22 | package-mode = false 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | pytest = "^8.4.1" 26 | pytest-timer = "^1.0.0" 27 | isort = "^6.0.1" 28 | black = "^25.1.0" 29 | pyright = "^1.1.402" 30 | freezegun = "^1.5.2" 31 | 32 | [tool.black] 33 | line-length = 120 34 | target-version = ['py312'] 35 | skip-magic-trailing-comma = true 36 | 37 | [tool.isort] 38 | profile = "black" 39 | line_length = 120 40 | atomic = true 41 | 42 | [tool.pyright] 43 | exclude = [ 44 | "tests" 45 | ] 46 | strictListInference = true 47 | strictSetInference = true 48 | # since the dictionary may contain various values, we disable the strict dictionary inference 49 | strictDictionaryInference = false 50 | deprecateTypingAliases = true 51 | reportImportCycles = true 52 | reportUnusedImport = true 53 | reportUnusedClass = true 54 | reportUnusedFunction = true 55 | reportUnusedVariable = true 56 | reportDuplicateImport = true 57 | reportDeprecated = true 58 | reportUnusedExpression = true 59 | 60 | [tool.pytest.ini_options] 61 | testpaths = [ 62 | "tests" 63 | ] 64 | markers = [ 65 | "unit", 66 | ] 67 | addopts = [ 68 | "-vv", 69 | "--timer-top-n=10" 70 | ] 71 | -------------------------------------------------------------------------------- /python_src/entrypoint/app/http_server.py: -------------------------------------------------------------------------------- 1 | """HTTP server setup and configuration.""" 2 | 3 | import structlog 4 | from fastapi import FastAPI 5 | 6 | from internal.router.handlers import create_api_router 7 | from internal.router.middleware import LoggingMiddleware, RequestIDMiddleware 8 | 9 | 10 | def configure_logging() -> structlog.BoundLogger: 11 | """Configure structured logging for the application.""" 12 | # Configure structlog 13 | structlog.configure( 14 | processors=[ 15 | structlog.contextvars.merge_contextvars, 16 | structlog.processors.add_log_level, 17 | structlog.processors.StackInfoRenderer(), 18 | structlog.dev.set_exc_info, 19 | structlog.processors.TimeStamper(fmt="iso"), 20 | structlog.dev.ConsoleRenderer(), 21 | ], 22 | wrapper_class=structlog.make_filtering_bound_logger(20), # INFO level 23 | logger_factory=structlog.PrintLoggerFactory(), 24 | cache_logger_on_first_use=True, 25 | ) 26 | 27 | # Create root logger 28 | logger = structlog.get_logger() 29 | return logger.bind(service="workshop", env="production") 30 | 31 | 32 | def create_app(logger: structlog.BoundLogger | None = None) -> FastAPI: 33 | """Create and configure FastAPI application.""" 34 | if logger is None: 35 | logger = configure_logging() 36 | 37 | app = FastAPI(title="Workshop API", version="1.0.0", docs_url="/docs", redoc_url="/redoc") 38 | 39 | # Add middleware (order matters - first added is outermost) 40 | app.add_middleware(LoggingMiddleware, logger=logger) 41 | app.add_middleware(RequestIDMiddleware) 42 | 43 | # Add API routes 44 | api_router = create_api_router() 45 | app.include_router(api_router) 46 | 47 | return app 48 | 49 | 50 | app = create_app() 51 | -------------------------------------------------------------------------------- /go_src/internal/domain/organization/bot.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Bot represents a channel entity associated with an organization. 8 | // It contains the configuration and credentials for integration across multiple platforms (LINE, Facebook, Instagram). 9 | type Bot struct { 10 | ID int `json:"id"` 11 | OrganizationID int `json:"organization_id"` 12 | Name string `json:"name"` 13 | Type BotType `json:"type"` 14 | ChannelID string `json:"channel_id"` 15 | ChannelSecret string `json:"channel_secret"` 16 | AccessToken string `json:"access_token"` 17 | TokenExpiredTime time.Time `json:"token_expired_time"` 18 | CreatedAt time.Time `json:"created_at"` 19 | UpdatedAt time.Time `json:"updated_at"` 20 | ExpiredAt *time.Time `json:"expired_at,omitempty"` 21 | Enable bool `json:"enable"` 22 | } 23 | 24 | // BotType represents the type of bot. 25 | type BotType string 26 | 27 | const ( 28 | BotTypeLine BotType = "LINE" 29 | BotTypeFacebook BotType = "FB" 30 | BotTypeInstagram BotType = "IG" 31 | // Add other bot types as needed 32 | ) 33 | 34 | // IsActive returns true if the bot is enabled and not expired. 35 | func (b *Bot) IsActive() bool { 36 | if !b.Enable { 37 | return false 38 | } 39 | if b.ExpiredAt != nil && time.Now().After(*b.ExpiredAt) { 40 | return false 41 | } 42 | return true 43 | } 44 | 45 | // IsTokenExpired returns true if the bot's access token has expired. 46 | func (b *Bot) IsTokenExpired() bool { 47 | return b.TokenExpiredTime.Before(time.Now()) 48 | } 49 | 50 | // IsTokenValid returns true if the bot has a valid access token. 51 | func (b *Bot) IsTokenValid() bool { 52 | return b.AccessToken != "" && !b.IsTokenExpired() 53 | } 54 | -------------------------------------------------------------------------------- /go_src/internal/router/middleware_zwsp.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // ref: https://github.com/trubitsyn/go-zero-width 13 | const ( 14 | // ZWSP represents zero-width space. 15 | ZWSP = string('\u200B') 16 | 17 | // ZWNBSP represents zero-width no-break space. 18 | ZWNBSP = string('\uFEFF') 19 | 20 | // ZWJ represents zero-width joiner. 21 | ZWJ = string('\u200D') 22 | 23 | // ZWNJ represents zero-width non-joiner. 24 | ZWNJ = string('\u200C') 25 | 26 | // UTF8NUL represents invalid byte sequence for encoding 0x00, which is not supported by some DBs. 27 | // ref: https://stackoverflow.com/questions/1347646/postgres-error-on-insert-error-invalid-byte-sequence-for-encoding-utf8-0x0 28 | UTF8NUL = string('\u0000') 29 | UTF8NULStr = "\\u0000" 30 | 31 | empty = "" 32 | ) 33 | 34 | var replacer = strings.NewReplacer( 35 | ZWSP, empty, 36 | ZWNBSP, empty, 37 | ZWJ, empty, 38 | ZWNJ, empty, 39 | UTF8NUL, empty, 40 | UTF8NULStr, empty) 41 | 42 | func RemoveZWSPMiddleware() gin.HandlerFunc { 43 | return func(c *gin.Context) { 44 | data, err := io.ReadAll(c.Request.Body) 45 | if err != nil { 46 | _ = c.AbortWithError(http.StatusInternalServerError, err) 47 | return 48 | } 49 | 50 | if len(data) > 0 && hasZWSP(data) { 51 | s := replacer.Replace(string(data)) 52 | c.Request.Body = io.NopCloser(bytes.NewBufferString(s)) 53 | } else { 54 | c.Request.Body = io.NopCloser(bytes.NewBuffer(data)) 55 | } 56 | 57 | c.Next() 58 | } 59 | } 60 | 61 | func hasZWSP(data []byte) bool { 62 | return bytes.ContainsAny(data, ZWSP) || 63 | bytes.ContainsAny(data, ZWNBSP) || 64 | bytes.ContainsAny(data, ZWNJ) || 65 | bytes.ContainsAny(data, UTF8NUL) || 66 | bytes.ContainsAny(data, UTF8NULStr) 67 | } 68 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/entrypoint/app/http_server.py: -------------------------------------------------------------------------------- 1 | """HTTP server setup and configuration.""" 2 | 3 | import structlog 4 | from fastapi import FastAPI 5 | 6 | from internal.router.handlers import create_api_router 7 | from internal.router.middleware import LoggingMiddleware, RequestIDMiddleware 8 | 9 | 10 | def configure_logging() -> structlog.BoundLogger: 11 | """Configure structured logging for the application.""" 12 | # Configure structlog 13 | structlog.configure( 14 | processors=[ 15 | structlog.contextvars.merge_contextvars, 16 | structlog.processors.add_log_level, 17 | structlog.processors.StackInfoRenderer(), 18 | structlog.dev.set_exc_info, 19 | structlog.processors.TimeStamper(fmt="iso"), 20 | structlog.dev.ConsoleRenderer(), 21 | ], 22 | wrapper_class=structlog.make_filtering_bound_logger(20), # INFO level 23 | logger_factory=structlog.PrintLoggerFactory(), 24 | cache_logger_on_first_use=True, 25 | ) 26 | 27 | # Create root logger 28 | logger = structlog.get_logger() 29 | return logger.bind(service="workshop", env="production") 30 | 31 | 32 | def create_app(logger: structlog.BoundLogger | None = None) -> FastAPI: 33 | """Create and configure FastAPI application.""" 34 | if logger is None: 35 | logger = configure_logging() 36 | 37 | app = FastAPI(title="Workshop API", version="1.0.0", docs_url="/docs", redoc_url="/redoc") 38 | 39 | # Add middleware (order matters - first added is outermost) 40 | app.add_middleware(LoggingMiddleware, logger=logger) 41 | app.add_middleware(RequestIDMiddleware) 42 | 43 | # Add API routes 44 | api_router = create_api_router() 45 | app.include_router(api_router) 46 | 47 | return app 48 | 49 | 50 | app = create_app() 51 | -------------------------------------------------------------------------------- /cheat_sheet/go/2_extend_feature/internal/domain/organization/bot.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Bot represents a channel entity associated with an organization. 8 | // It contains the configuration and credentials for integration across multiple platforms (LINE, Facebook, Instagram). 9 | type Bot struct { 10 | ID int `json:"id"` 11 | OrganizationID int `json:"organization_id"` 12 | Name string `json:"name"` 13 | Type BotType `json:"type"` 14 | ChannelID string `json:"channel_id"` 15 | ChannelSecret string `json:"channel_secret"` 16 | AccessToken string `json:"access_token"` 17 | TokenExpiredTime time.Time `json:"token_expired_time"` 18 | CreatedAt time.Time `json:"created_at"` 19 | UpdatedAt time.Time `json:"updated_at"` 20 | ExpiredAt *time.Time `json:"expired_at,omitempty"` 21 | Enable bool `json:"enable"` 22 | } 23 | 24 | // BotType represents the type of bot. 25 | type BotType string 26 | 27 | const ( 28 | BotTypeLine BotType = "LINE" 29 | BotTypeFacebook BotType = "FB" 30 | BotTypeInstagram BotType = "IG" 31 | // Add other bot types as needed 32 | ) 33 | 34 | // IsActive returns true if the bot is enabled and not expired. 35 | func (b *Bot) IsActive() bool { 36 | if !b.Enable { 37 | return false 38 | } 39 | if b.ExpiredAt != nil && time.Now().After(*b.ExpiredAt) { 40 | return false 41 | } 42 | return true 43 | } 44 | 45 | // IsTokenExpired returns true if the bot's access token has expired. 46 | func (b *Bot) IsTokenExpired() bool { 47 | return b.TokenExpiredTime.Before(time.Now()) 48 | } 49 | 50 | // IsTokenValid returns true if the bot has a valid access token. 51 | func (b *Bot) IsTokenValid() bool { 52 | return b.AccessToken != "" && !b.IsTokenExpired() 53 | } 54 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/entrypoint/app/http_server.py: -------------------------------------------------------------------------------- 1 | """HTTP server setup and configuration.""" 2 | 3 | import structlog 4 | from fastapi import FastAPI 5 | 6 | from internal.router.handlers import create_api_router 7 | from internal.router.middleware import LoggingMiddleware, RequestIDMiddleware 8 | 9 | 10 | def configure_logging() -> structlog.BoundLogger: 11 | """Configure structured logging for the application.""" 12 | # Configure structlog 13 | structlog.configure( 14 | processors=[ 15 | structlog.contextvars.merge_contextvars, 16 | structlog.processors.add_log_level, 17 | structlog.processors.StackInfoRenderer(), 18 | structlog.dev.set_exc_info, 19 | structlog.processors.TimeStamper(fmt="iso"), 20 | structlog.dev.ConsoleRenderer(), 21 | ], 22 | wrapper_class=structlog.make_filtering_bound_logger(20), # INFO level 23 | logger_factory=structlog.PrintLoggerFactory(), 24 | cache_logger_on_first_use=True, 25 | ) 26 | 27 | # Create root logger 28 | logger = structlog.get_logger() 29 | return logger.bind(service="workshop", env="production") 30 | 31 | 32 | def create_app(logger: structlog.BoundLogger | None = None) -> FastAPI: 33 | """Create and configure FastAPI application.""" 34 | if logger is None: 35 | logger = configure_logging() 36 | 37 | app = FastAPI(title="Workshop API", version="1.0.0", docs_url="/docs", redoc_url="/redoc") 38 | 39 | # Add middleware (order matters - first added is outermost) 40 | app.add_middleware(LoggingMiddleware, logger=logger) 41 | app.add_middleware(RequestIDMiddleware) 42 | 43 | # Add API routes 44 | api_router = create_api_router() 45 | app.include_router(api_router) 46 | 47 | return app 48 | 49 | 50 | app = create_app() 51 | -------------------------------------------------------------------------------- /cheat_sheet/go/1_rewrite_brownfield/internal/domain/organization/bot.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Bot represents a channel entity associated with an organization. 8 | // It contains the configuration and credentials for integration across multiple platforms (LINE, Facebook, Instagram). 9 | type Bot struct { 10 | ID int `json:"id"` 11 | OrganizationID int `json:"organization_id"` 12 | Name string `json:"name"` 13 | Type BotType `json:"type"` 14 | ChannelID string `json:"channel_id"` 15 | ChannelSecret string `json:"channel_secret"` 16 | AccessToken string `json:"access_token"` 17 | TokenExpiredTime time.Time `json:"token_expired_time"` 18 | CreatedAt time.Time `json:"created_at"` 19 | UpdatedAt time.Time `json:"updated_at"` 20 | ExpiredAt *time.Time `json:"expired_at,omitempty"` 21 | Enable bool `json:"enable"` 22 | } 23 | 24 | // BotType represents the type of bot. 25 | type BotType string 26 | 27 | const ( 28 | BotTypeLine BotType = "LINE" 29 | BotTypeFacebook BotType = "FB" 30 | BotTypeInstagram BotType = "IG" 31 | // Add other bot types as needed 32 | ) 33 | 34 | // IsActive returns true if the bot is enabled and not expired. 35 | func (b *Bot) IsActive() bool { 36 | if !b.Enable { 37 | return false 38 | } 39 | if b.ExpiredAt != nil && time.Now().After(*b.ExpiredAt) { 40 | return false 41 | } 42 | return true 43 | } 44 | 45 | // IsTokenExpired returns true if the bot's access token has expired. 46 | func (b *Bot) IsTokenExpired() bool { 47 | return b.TokenExpiredTime.Before(time.Now()) 48 | } 49 | 50 | // IsTokenValid returns true if the bot has a valid access token. 51 | func (b *Bot) IsTokenValid() bool { 52 | return b.AccessToken != "" && !b.IsTokenExpired() 53 | } 54 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ai-coding-workshop-250712" 3 | version = "0.1.0" 4 | description = "" 5 | requires-python = ">=3.12" 6 | dependencies = [ 7 | "pydantic (>=2.11.7,<3.0.0)", 8 | "pytz (>=2025.2,<2026.0)", 9 | "pydantic-settings (>=2.10.1,<3.0.0)", 10 | "httpx (>=0.28.1,<0.29.0)", 11 | "uvicorn[standard] (>=0.35.0,<0.36.0)", 12 | "structlog (>=25.4.0,<26.0.0)", 13 | "fastapi (>=0.115.14,<0.116.0)" 14 | ] 15 | 16 | 17 | [build-system] 18 | requires = ["poetry-core>=2.0.0,<3.0.0"] 19 | build-backend = "poetry.core.masonry.api" 20 | 21 | [tool.poetry] 22 | package-mode = false 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | pytest = "^8.4.1" 26 | pytest-timer = "^1.0.0" 27 | isort = "^6.0.1" 28 | black = "^25.1.0" 29 | pyright = "^1.1.402" 30 | freezegun = "^1.5.2" 31 | 32 | [tool.black] 33 | line-length = 120 34 | target-version = ['py312'] 35 | skip-magic-trailing-comma = true 36 | #include = ''' 37 | #( 38 | # /( 39 | # python_src 40 | # )/ 41 | #) 42 | #''' 43 | 44 | [tool.isort] 45 | profile = "black" 46 | line_length = 120 47 | atomic = true 48 | #src_paths = ["python_src"] 49 | 50 | [tool.pyright] 51 | #include = ["python_src"] 52 | strictListInference = true 53 | strictSetInference = true 54 | # since the dictionary may contain various values, we disable the strict dictionary inference 55 | strictDictionaryInference = false 56 | deprecateTypingAliases = true 57 | reportImportCycles = true 58 | reportUnusedImport = true 59 | reportUnusedClass = true 60 | reportUnusedFunction = true 61 | reportUnusedVariable = true 62 | reportDuplicateImport = true 63 | reportDeprecated = true 64 | reportUnusedExpression = true 65 | 66 | [tool.pytest.ini_options] 67 | #pythonpath = [ 68 | # "python_src" 69 | #] 70 | #testpaths = [ 71 | # "tests" 72 | #] 73 | markers = [ 74 | "unit", 75 | ] 76 | addopts = [ 77 | "-vv", 78 | "--timer-top-n=10" 79 | ] 80 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ai-coding-workshop-250712" 3 | version = "0.1.0" 4 | description = "" 5 | requires-python = ">=3.12" 6 | dependencies = [ 7 | "pydantic (>=2.11.7,<3.0.0)", 8 | "pytz (>=2025.2,<2026.0)", 9 | "pydantic-settings (>=2.10.1,<3.0.0)", 10 | "httpx (>=0.28.1,<0.29.0)", 11 | "uvicorn[standard] (>=0.35.0,<0.36.0)", 12 | "structlog (>=25.4.0,<26.0.0)", 13 | "fastapi (>=0.115.14,<0.116.0)" 14 | ] 15 | 16 | 17 | [build-system] 18 | requires = ["poetry-core>=2.0.0,<3.0.0"] 19 | build-backend = "poetry.core.masonry.api" 20 | 21 | [tool.poetry] 22 | package-mode = false 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | pytest = "^8.4.1" 26 | pytest-timer = "^1.0.0" 27 | isort = "^6.0.1" 28 | black = "^25.1.0" 29 | pyright = "^1.1.402" 30 | freezegun = "^1.5.2" 31 | 32 | [tool.black] 33 | line-length = 120 34 | target-version = ['py312'] 35 | skip-magic-trailing-comma = true 36 | #include = ''' 37 | #( 38 | # /( 39 | # python_src 40 | # )/ 41 | #) 42 | #''' 43 | 44 | [tool.isort] 45 | profile = "black" 46 | line_length = 120 47 | atomic = true 48 | #src_paths = ["python_src"] 49 | 50 | [tool.pyright] 51 | #include = ["python_src"] 52 | strictListInference = true 53 | strictSetInference = true 54 | # since the dictionary may contain various values, we disable the strict dictionary inference 55 | strictDictionaryInference = false 56 | deprecateTypingAliases = true 57 | reportImportCycles = true 58 | reportUnusedImport = true 59 | reportUnusedClass = true 60 | reportUnusedFunction = true 61 | reportUnusedVariable = true 62 | reportDuplicateImport = true 63 | reportDeprecated = true 64 | reportUnusedExpression = true 65 | 66 | [tool.pytest.ini_options] 67 | #pythonpath = [ 68 | # "python_src" 69 | #] 70 | #testpaths = [ 71 | # "tests" 72 | #] 73 | markers = [ 74 | "unit", 75 | ] 76 | addopts = [ 77 | "-vv", 78 | "--timer-top-n=10" 79 | ] 80 | -------------------------------------------------------------------------------- /go_src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chatbotgang/workshop 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/alecthomas/kingpin/v2 v2.4.0 7 | github.com/gin-gonic/gin v1.10.1 8 | github.com/go-playground/validator/v10 v10.27.0 9 | github.com/google/uuid v1.6.0 10 | github.com/rs/zerolog v1.34.0 11 | github.com/stretchr/testify v1.10.0 12 | ) 13 | 14 | require ( 15 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 16 | github.com/bytedance/sonic v1.11.6 // indirect 17 | github.com/bytedance/sonic/loader v0.1.1 // indirect 18 | github.com/cloudwego/base64x v0.1.4 // indirect 19 | github.com/cloudwego/iasm v0.2.0 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 22 | github.com/gin-contrib/sse v0.1.0 // indirect 23 | github.com/go-playground/locales v0.14.1 // indirect 24 | github.com/go-playground/universal-translator v0.18.1 // indirect 25 | github.com/goccy/go-json v0.10.2 // indirect 26 | github.com/json-iterator/go v1.1.12 // indirect 27 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 28 | github.com/leodido/go-urn v1.4.0 // indirect 29 | github.com/mattn/go-colorable v0.1.13 // indirect 30 | github.com/mattn/go-isatty v0.0.20 // indirect 31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 32 | github.com/modern-go/reflect2 v1.0.2 // indirect 33 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 34 | github.com/pmezard/go-difflib v1.0.0 // indirect 35 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 36 | github.com/ugorji/go/codec v1.2.12 // indirect 37 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 38 | golang.org/x/arch v0.8.0 // indirect 39 | golang.org/x/crypto v0.33.0 // indirect 40 | golang.org/x/net v0.34.0 // indirect 41 | golang.org/x/sys v0.30.0 // indirect 42 | golang.org/x/text v0.22.0 // indirect 43 | google.golang.org/protobuf v1.34.1 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /.github/workflows/python_ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ main, develop ] 6 | paths: 7 | - 'python_src/**' 8 | - '.github/workflows/python_ci.yml' 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | lint: 16 | name: Python Lint 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Install poetry 24 | run: pipx install poetry 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version-file: 'python_src/pyproject.toml' 30 | cache: "poetry" 31 | 32 | - name: Install dependencies 33 | working-directory: python_src 34 | run: poetry install --no-interaction --no-root 35 | 36 | - name: Run isort 37 | working-directory: python_src 38 | run: poetry run isort . --check --diff 39 | 40 | - name: Run black 41 | working-directory: python_src 42 | run: poetry run black . --check --diff 43 | 44 | - name: Run pyright 45 | working-directory: python_src 46 | run: poetry run pyright 47 | 48 | test: 49 | name: Integration test and unit test 50 | runs-on: ubuntu-latest 51 | 52 | steps: 53 | - name: Checkout 54 | uses: actions/checkout@v4 55 | 56 | - name: Install poetry 57 | run: pipx install poetry 58 | 59 | - name: Set up Python 60 | uses: actions/setup-python@v5 61 | with: 62 | python-version-file: 'python_src/pyproject.toml' 63 | cache: "poetry" 64 | 65 | - name: Install dependencies 66 | working-directory: python_src 67 | run: poetry install --no-interaction --no-root 68 | 69 | - name: Run pytest 70 | working-directory: python_src 71 | run: poetry run pytest 72 | env: 73 | PYTHONPATH: "." -------------------------------------------------------------------------------- /go_src/cmd/app/http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/rs/zerolog" 12 | 13 | "github.com/chatbotgang/workshop/internal/app" 14 | "github.com/chatbotgang/workshop/internal/router" 15 | "github.com/chatbotgang/workshop/internal/router/validator" 16 | ) 17 | 18 | func runHTTPServer(rootCtx context.Context, wg *sync.WaitGroup, port int, app *app.Application) { 19 | // Set to release mode to disable Gin logger 20 | gin.SetMode(gin.ReleaseMode) 21 | 22 | // Create gin router 23 | ginRouter := gin.New() 24 | 25 | // Set general middleware 26 | router.SetGeneralMiddlewares(rootCtx, ginRouter) 27 | 28 | // Register all handlers 29 | router.RegisterHandlers(ginRouter, app) 30 | 31 | validator.RegisteValidator() 32 | 33 | // Build HTTP server 34 | httpAddr := fmt.Sprintf("0.0.0.0:%d", port) 35 | server := &http.Server{ 36 | Addr: httpAddr, 37 | Handler: ginRouter, 38 | ReadHeaderTimeout: 5 * time.Second, // G112: fix potential Slowloris attack 39 | } 40 | 41 | // Run the server in a goroutine 42 | go func() { 43 | zerolog.Ctx(rootCtx).Info().Msgf("HTTP server is on http://%s", httpAddr) 44 | err := server.ListenAndServe() 45 | if err != nil && err != http.ErrServerClosed { 46 | zerolog.Ctx(rootCtx).Panic().Err(err).Str("addr", httpAddr).Msg("fail to start HTTP server") 47 | } 48 | }() 49 | 50 | // Wait for rootCtx done 51 | go func() { 52 | <-rootCtx.Done() 53 | 54 | // Graceful shutdown http server with a timeout 55 | zerolog.Ctx(rootCtx).Info().Msgf("HTTP server is closing") 56 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 57 | defer cancel() 58 | if err := server.Shutdown(ctx); err != nil { 59 | zerolog.Ctx(ctx).Error().Err(err).Msg("fail to shutdown HTTP server") 60 | } 61 | 62 | // Notify when server is closed 63 | zerolog.Ctx(rootCtx).Info().Msgf("HTTP server is closed") 64 | wg.Done() 65 | }() 66 | } 67 | -------------------------------------------------------------------------------- /go_src/internal/domain/common/error_code.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "net/http" 4 | 5 | type ErrorCode struct { 6 | Name string 7 | StatusCode int 8 | } 9 | 10 | /* 11 | General error codes 12 | */ 13 | 14 | var ErrorCodeInternalProcess = ErrorCode{ 15 | Name: "INTERNAL_PROCESS", 16 | StatusCode: http.StatusInternalServerError, 17 | } 18 | 19 | /* 20 | Authentication and Authorization error codes 21 | */ 22 | 23 | var ErrorCodeAuthPermissionDenied = ErrorCode{ 24 | Name: "AUTH_PERMISSION_DENIED", 25 | StatusCode: http.StatusForbidden, 26 | } 27 | 28 | var ErrorCodeAuthNotAuthenticated = ErrorCode{ 29 | Name: "AUTH_NOT_AUTHENTICATED", 30 | StatusCode: http.StatusUnauthorized, 31 | } 32 | 33 | /* 34 | Resource-related error codes 35 | */ 36 | 37 | var ErrorCodeResourceNotFound = ErrorCode{ 38 | Name: "RESOURCE_NOT_FOUND", 39 | StatusCode: http.StatusNotFound, 40 | } 41 | 42 | /* 43 | Parameter-related error codes 44 | */ 45 | 46 | var ErrorCodeParameterInvalid = ErrorCode{ 47 | Name: "PARAMETER_INVALID", 48 | StatusCode: http.StatusBadRequest, 49 | } 50 | 51 | var ErrorCodeMessageContentInvalid = ErrorCode{ 52 | Name: "MESSAGE_CONTENT_INVALID", 53 | StatusCode: http.StatusBadRequest, 54 | } 55 | 56 | var ErrorCodeUnsupportedChannelType = ErrorCode{ 57 | Name: "UNSUPPORTED_CHANNEL_TYPE", 58 | StatusCode: http.StatusBadRequest, 59 | } 60 | 61 | /* 62 | Remote server-related error codes 63 | */ 64 | 65 | var ErrorCodeRemoteNetworkError = ErrorCode{ 66 | Name: "REMOTE_NETWORK_ERROR", 67 | StatusCode: http.StatusBadGateway, 68 | } 69 | 70 | var ErrorCodeRemoteNetworkErrorNoRetry = ErrorCode{ 71 | Name: "REMOTE_NETWORK_ERROR_NO_RETRY", 72 | StatusCode: http.StatusBadGateway, 73 | } 74 | 75 | var ErrorCodeRemoteClientError = ErrorCode{ 76 | Name: "REMOTE_CLIENT_ERROR", 77 | StatusCode: http.StatusBadRequest, 78 | } 79 | 80 | var ErrorCodeRemoteServerError = ErrorCode{ 81 | Name: "REMOTE_SERVER_ERROR", 82 | StatusCode: http.StatusBadGateway, 83 | } 84 | -------------------------------------------------------------------------------- /go_src/internal/domain/auto_reply/auto_reply.go: -------------------------------------------------------------------------------- 1 | package auto_reply 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // AutoReply represents an omnichannel rule that associates several WebhookTriggerSetting instances. 8 | // It defines the high-level auto-reply configuration for an organization. 9 | type AutoReply struct { 10 | ID int `json:"id"` 11 | OrganizationID int `json:"organization_id"` 12 | Name string `json:"name"` 13 | Status AutoReplyStatus `json:"status"` 14 | EventType AutoReplyEventType `json:"event_type"` 15 | Priority int `json:"priority"` 16 | Keywords []string `json:"keywords,omitempty"` 17 | TriggerScheduleType *WebhookTriggerScheduleType `json:"trigger_schedule_type,omitempty"` 18 | TriggerScheduleSettings *WebhookTriggerScheduleSettings `json:"trigger_schedule_settings,omitempty"` 19 | CreatedAt time.Time `json:"created_at"` 20 | UpdatedAt time.Time `json:"updated_at"` 21 | } 22 | 23 | // AutoReplyStatus represents the status of an auto-reply rule. 24 | type AutoReplyStatus string 25 | 26 | const ( 27 | AutoReplyStatusActive AutoReplyStatus = "active" 28 | AutoReplyStatusInactive AutoReplyStatus = "inactive" 29 | AutoReplyStatusArchived AutoReplyStatus = "archived" 30 | ) 31 | 32 | // AutoReplyEventType represents the type of event that triggers the auto-reply. 33 | type AutoReplyEventType string 34 | 35 | const ( 36 | AutoReplyEventTypeMessage AutoReplyEventType = "message" 37 | AutoReplyEventTypePostback AutoReplyEventType = "postback" 38 | AutoReplyEventTypeFollow AutoReplyEventType = "follow" 39 | AutoReplyEventTypeBeacon AutoReplyEventType = "beacon" 40 | AutoReplyEventTypeTime AutoReplyEventType = "time" 41 | AutoReplyEventTypeKeyword AutoReplyEventType = "keyword" 42 | AutoReplyEventTypeDefault AutoReplyEventType = "default" 43 | ) 44 | -------------------------------------------------------------------------------- /cheat_sheet/go/2_extend_feature/internal/domain/auto_reply/auto_reply.go: -------------------------------------------------------------------------------- 1 | package auto_reply 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // AutoReply represents an omnichannel rule that associates several WebhookTriggerSetting instances. 8 | // It defines the high-level auto-reply configuration for an organization. 9 | type AutoReply struct { 10 | ID int `json:"id"` 11 | OrganizationID int `json:"organization_id"` 12 | Name string `json:"name"` 13 | Status AutoReplyStatus `json:"status"` 14 | EventType AutoReplyEventType `json:"event_type"` 15 | Priority int `json:"priority"` 16 | Keywords []string `json:"keywords,omitempty"` 17 | TriggerScheduleType *WebhookTriggerScheduleType `json:"trigger_schedule_type,omitempty"` 18 | TriggerScheduleSettings *WebhookTriggerScheduleSettings `json:"trigger_schedule_settings,omitempty"` 19 | CreatedAt time.Time `json:"created_at"` 20 | UpdatedAt time.Time `json:"updated_at"` 21 | } 22 | 23 | // AutoReplyStatus represents the status of an auto-reply rule. 24 | type AutoReplyStatus string 25 | 26 | const ( 27 | AutoReplyStatusActive AutoReplyStatus = "active" 28 | AutoReplyStatusInactive AutoReplyStatus = "inactive" 29 | AutoReplyStatusArchived AutoReplyStatus = "archived" 30 | ) 31 | 32 | // AutoReplyEventType represents the type of event that triggers the auto-reply. 33 | type AutoReplyEventType string 34 | 35 | const ( 36 | AutoReplyEventTypeMessage AutoReplyEventType = "message" 37 | AutoReplyEventTypePostback AutoReplyEventType = "postback" 38 | AutoReplyEventTypeFollow AutoReplyEventType = "follow" 39 | AutoReplyEventTypeBeacon AutoReplyEventType = "beacon" 40 | AutoReplyEventTypeTime AutoReplyEventType = "time" 41 | AutoReplyEventTypeKeyword AutoReplyEventType = "keyword" 42 | AutoReplyEventTypeDefault AutoReplyEventType = "default" 43 | ) 44 | -------------------------------------------------------------------------------- /cheat_sheet/go/1_rewrite_brownfield/internal/domain/auto_reply/auto_reply.go: -------------------------------------------------------------------------------- 1 | package auto_reply 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // AutoReply represents an omnichannel rule that associates several WebhookTriggerSetting instances. 8 | // It defines the high-level auto-reply configuration for an organization. 9 | type AutoReply struct { 10 | ID int `json:"id"` 11 | OrganizationID int `json:"organization_id"` 12 | Name string `json:"name"` 13 | Status AutoReplyStatus `json:"status"` 14 | EventType AutoReplyEventType `json:"event_type"` 15 | Priority int `json:"priority"` 16 | Keywords []string `json:"keywords,omitempty"` 17 | TriggerScheduleType *WebhookTriggerScheduleType `json:"trigger_schedule_type,omitempty"` 18 | TriggerScheduleSettings *WebhookTriggerScheduleSettings `json:"trigger_schedule_settings,omitempty"` 19 | CreatedAt time.Time `json:"created_at"` 20 | UpdatedAt time.Time `json:"updated_at"` 21 | } 22 | 23 | // AutoReplyStatus represents the status of an auto-reply rule. 24 | type AutoReplyStatus string 25 | 26 | const ( 27 | AutoReplyStatusActive AutoReplyStatus = "active" 28 | AutoReplyStatusInactive AutoReplyStatus = "inactive" 29 | AutoReplyStatusArchived AutoReplyStatus = "archived" 30 | ) 31 | 32 | // AutoReplyEventType represents the type of event that triggers the auto-reply. 33 | type AutoReplyEventType string 34 | 35 | const ( 36 | AutoReplyEventTypeMessage AutoReplyEventType = "message" 37 | AutoReplyEventTypePostback AutoReplyEventType = "postback" 38 | AutoReplyEventTypeFollow AutoReplyEventType = "follow" 39 | AutoReplyEventTypeBeacon AutoReplyEventType = "beacon" 40 | AutoReplyEventTypeTime AutoReplyEventType = "time" 41 | AutoReplyEventTypeKeyword AutoReplyEventType = "keyword" 42 | AutoReplyEventTypeDefault AutoReplyEventType = "default" 43 | ) 44 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths: 7 | - 'go_src/**' 8 | - '.github/workflows/go.yml' 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | GOLANGCI_LINT_VERSION: v2.2.1 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup Golang with cache 24 | uses: magnetikonline/action-golang-cache@v5 25 | with: 26 | go-version-file: go_src/go.mod 27 | 28 | - name: Build 29 | run: (cd go_src/ && make build) 30 | 31 | lint-internal: 32 | runs-on: ubuntu-latest 33 | needs: [build] 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - name: Setup Golang with cache 38 | uses: magnetikonline/action-golang-cache@v5 39 | with: 40 | go-version-file: go_src/go.mod 41 | 42 | - name: Run golangci-lint in internal 43 | uses: golangci/golangci-lint-action@v7 44 | with: 45 | version: ${{ env.GOLANGCI_LINT_VERSION }} 46 | args: --timeout=10m ./internal/... 47 | working-directory: go_src 48 | 49 | lint-cmd: 50 | runs-on: ubuntu-latest 51 | needs: [build] 52 | steps: 53 | - uses: actions/checkout@v4 54 | 55 | - name: Setup Golang with cache 56 | uses: magnetikonline/action-golang-cache@v5 57 | with: 58 | go-version-file: go_src/go.mod 59 | 60 | - name: Run golangci-lint in cmd 61 | uses: golangci/golangci-lint-action@v7 62 | with: 63 | version: ${{ env.GOLANGCI_LINT_VERSION }} 64 | args: --timeout=10m ./cmd/... 65 | working-directory: go_src 66 | 67 | tests: 68 | runs-on: ubuntu-latest 69 | needs: [build] 70 | steps: 71 | - uses: actions/checkout@v4 72 | 73 | - name: Setup Golang with cache 74 | uses: magnetikonline/action-golang-cache@v5 75 | with: 76 | go-version-file: go_src/go.mod 77 | 78 | - name: Run tests 79 | run: (cd go_src/ && make test) 80 | -------------------------------------------------------------------------------- /.ai/prompt/dev_with_kb.prompt.md: -------------------------------------------------------------------------------- 1 | --- 2 | mode: 'agent' 3 | --- 4 | ## AI-Guided Feature Development Workflow with Feature KB 5 | 6 | You are an expert software engineer assistant. 7 | 8 | You are given: 9 | - A **New Spec** (feature requirement) 10 | - A **Feature Knowledge Base (Feature KB)** that describes the current system 11 | 12 | Your task is to help develop the new feature through the following phases: 13 | 14 | --- 15 | 16 | 🧩 Phase 1: Clarification 17 | 18 | Based on the New Spec and the Feature KB: 19 | 20 | - Identify critical clarifications or hidden assumptions. 21 | - Ask only **one question at a time**. 22 | - **Do not proceed** until the previous question is clearly answered. 23 | - Provide **multiple-choice options** to speed up iteration. 24 | - Example format for multiple-choice options: 25 | - **Question:** What is the primary purpose of this feature? 26 | - (A) To handle user authentication. 27 | - (B) To process payment transactions. 28 | - (C) To manage user notifications. 29 | - (D) Other (please specify). 30 | 31 | --- 32 | 33 | 🛠️ Phase 2: Development Plan 34 | 35 | Once clarifications are resolved: 36 | 37 | - Generate a **concise development plan**, broken into sequential tasks. 38 | - Each task should be **small and focused**. 39 | - Format example: 40 | 41 | task 1: Add new database field for user metadata 42 | task 2: Update API to accept and store the field 43 | task 3: Add validation and tests 44 | 45 | --- 46 | 47 | 💻 Phase 3: Code Implementation 48 | 49 | Implement tasks one by one: 50 | 51 | - Start with task 1. 52 | - Explain what you’re doing. 53 | - Show the code (with inline comments if helpful). 54 | - Include basic tests if applicable. 55 | - Ask for human confirmation before continuing to the next task. 56 | 57 | --- 58 | 59 | 🧠 Phase 4: Suggest Feature KB Updates 60 | 61 | After final task: 62 | 63 | - Suggest changes to the Feature KB. 64 | - Include updated behaviors, edge cases, or technical traps introduced. 65 | - Format the suggestions clearly for direct insertion into the KB. 66 | 67 | --- 68 | 69 | 🧾 Context Input 70 | 71 | **New Spec:** 72 | [Insert your spec here] 73 | 74 | **Feature KB:** 75 | [Insert current KB or relevant excerpt here] 76 | -------------------------------------------------------------------------------- /.ai/prompt/prd_spec_to_user_story.prompt.md: -------------------------------------------------------------------------------- 1 | You are an expert technical project manager. 2 | 3 | You are given a **New Spec** (product requirement), and your goal is to support cross-functional understanding by breaking it down into fine-grained, actionable user stories organized by functionality. 4 | 5 | Your responsibilities: 6 | 7 | - Identify any critical clarifications or hidden assumptions that may affect implementation. 8 | - Ask only **one question at a time** to avoid overwhelming the discussion. 9 | - **Do not continue** until the current question is clearly answered. 10 | - Offer **multiple-choice options** when possible to accelerate alignment and decision-making. 11 | - If there are questions that cannot be answered or require broader input, add them to an **"Unresolved Questions"** list for stakeholder review. Do not assume or speculate. 12 | 13 | Once all necessary clarifications are complete, generate a list of **detailed user stories**, with the following requirements: 14 | 15 | - Organize the stories by functionality (feature groupings). 16 | - Each story should follow the format: 17 | “As a [user], I can [do something], so that [value is achieved].” 18 | - Assign a priority to each story: 19 | - **P0** – Must-have 20 | - **P1** – Nice-to-have 21 | - **P2** – Optional or future enhancement 22 | 23 | Example Output Format 24 | User Stories 25 | 26 | Auto-Reply Configuration 27 | - [P0] As a customer, I can create an auto-reply setting with multiple keywords, so that one reply can be triggered by several phrases. 28 | - [P0] As a customer, I can set a unique priority for each auto-reply setting per bot, so that I control which reply is triggered when keywords overlap. 29 | - [P1] As a customer, I can update all priorities for my bot in a single batch operation, and the system will enforce uniqueness and atomicity. 30 | - [P1] As a customer, I can update the keyword list for a setting, and the system will normalize and validate my input. 31 | 32 | Message History 33 | - [P0] As a customer, I can view which auto-reply setting was triggered for each incoming message, so that I can debug the behavior. 34 | - [P2] As a customer, I can export matched message logs to CSV for offline analysis. 35 | 36 | Unresolved Questions 37 | - Q1: Should users be allowed to create overlapping keyword sets across bots? 38 | - Q2: Do we allow emoji or non-ASCII characters in keywords? 39 | -------------------------------------------------------------------------------- /go_src/internal/router/param_util.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/chatbotgang/workshop/internal/domain/common" 11 | ) 12 | 13 | const KeyAuth = "Authorization" 14 | const KeyCredential = "Credential" 15 | 16 | func GetAuthorizationToken(c *gin.Context) (string, common.Error) { 17 | token := c.GetHeader(KeyAuth) 18 | if token == "" { 19 | msg := "no Authorization" 20 | return "", common.NewError(common.ErrorCodeAuthNotAuthenticated, errors.New(msg), common.WithMsg(msg)) 21 | } 22 | return token, nil 23 | } 24 | 25 | // GetParamInt gets a key's value from Gin's URL param and transform it to int. 26 | func GetParamInt(c *gin.Context, key string) (int, common.Error) { 27 | s := c.Param(key) 28 | if s == "" { 29 | msg := fmt.Sprintf("no %s", key) 30 | return 0, common.NewError(common.ErrorCodeParameterInvalid, errors.New(msg), common.WithMsg(msg)) 31 | } 32 | 33 | i, err := strconv.Atoi(s) 34 | if err != nil { 35 | msg := fmt.Sprintf("invalid %s", key) 36 | return 0, common.NewError(common.ErrorCodeParameterInvalid, errors.New(msg), common.WithMsg(msg)) 37 | } 38 | return i, nil 39 | } 40 | 41 | // GetQueryInt gets a key's value from Gin's URL query and transform it to int. 42 | func GetQueryInt(c *gin.Context, key string) (int, common.Error) { 43 | s := c.Query(key) 44 | if s == "" { 45 | msg := fmt.Sprintf("no %s", key) 46 | return 0, common.NewError(common.ErrorCodeParameterInvalid, errors.New(msg), common.WithMsg(msg)) 47 | } 48 | 49 | i, err := strconv.Atoi(s) 50 | if err != nil { 51 | msg := fmt.Sprintf("invalid %s", key) 52 | return 0, common.NewError(common.ErrorCodeParameterInvalid, errors.New(msg), common.WithMsg(msg)) 53 | } 54 | return i, nil 55 | } 56 | 57 | // GetQueryBool gets a key's value from Gin's URL query and transform it to bool. 58 | func GetQueryBool(c *gin.Context, key string) (bool, common.Error) { 59 | s := c.Query(key) 60 | if s == "" { 61 | msg := fmt.Sprintf("no %s", key) 62 | return false, common.NewError(common.ErrorCodeParameterInvalid, errors.New(msg), common.WithMsg(msg)) 63 | } 64 | 65 | b, err := strconv.ParseBool(s) 66 | if err != nil { 67 | msg := fmt.Sprintf("invalid %s", key) 68 | return false, common.NewError(common.ErrorCodeParameterInvalid, errors.New(msg), common.WithMsg(msg)) 69 | } 70 | return b, nil 71 | } 72 | -------------------------------------------------------------------------------- /go_src/internal/domain/common/error.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Error indicates a domain error 10 | type Error interface { 11 | Error() string 12 | Name() string 13 | ClientMsg() string 14 | } 15 | 16 | // DomainError used for expressing errors occurring in application. 17 | type DomainError struct { 18 | code ErrorCode // code indicates an ErrorCode customized for domain logic. 19 | err error // err contains a native error. It will be logged in system logs. 20 | clientMsg string // clientMsg contains a message that will return to clients 21 | remoteStatus int // remoteStatus contains proxy HTTP status code. It is used for remote process related errors. 22 | detail map[string]interface{} // detail contains some details that clients may need. It is business-driven. 23 | } 24 | 25 | func NewError(code ErrorCode, err error, opts ...ErrorOption) Error { 26 | e := DomainError{code: code, err: err} 27 | for _, o := range opts { 28 | o(&e) 29 | } 30 | return e 31 | } 32 | 33 | func (e DomainError) Error() string { 34 | var msgs []string 35 | if e.remoteStatus != 0 { 36 | msgs = append(msgs, strconv.Itoa(e.remoteStatus)) 37 | } 38 | if e.err != nil { 39 | msgs = append(msgs, e.err.Error()) 40 | } 41 | if e.clientMsg != "" { 42 | msgs = append(msgs, e.clientMsg) 43 | } 44 | 45 | return strings.Join(msgs, ": ") 46 | } 47 | 48 | func (e DomainError) Name() string { 49 | if e.code.Name == "" { 50 | return "UNKNOWN_ERROR" 51 | } 52 | return e.code.Name 53 | } 54 | 55 | func (e DomainError) ClientMsg() string { 56 | return e.clientMsg 57 | } 58 | 59 | func (e DomainError) HTTPStatus() int { 60 | if e.code.StatusCode == 0 { 61 | return http.StatusInternalServerError 62 | } 63 | return e.code.StatusCode 64 | } 65 | 66 | func (e DomainError) RemoteHTTPStatus() int { 67 | return e.remoteStatus 68 | } 69 | 70 | func (e DomainError) Detail() map[string]interface{} { 71 | return e.detail 72 | } 73 | 74 | type ErrorOption func(*DomainError) 75 | 76 | func WithMsg(msg string) ErrorOption { 77 | return func(e *DomainError) { 78 | e.clientMsg = msg 79 | } 80 | } 81 | 82 | func WithStatus(status int) ErrorOption { 83 | return func(e *DomainError) { 84 | e.remoteStatus = status 85 | } 86 | } 87 | 88 | func WithDetail(detail map[string]interface{}) ErrorOption { 89 | return func(e *DomainError) { 90 | e.detail = detail 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /.ai/prompt/prd_user_story_to_use_case.prompt.md: -------------------------------------------------------------------------------- 1 | You are a lead engineer and QA lead. 2 | 3 | You will be given user stories, typically from a Product Requirements Document (PRD). The user stories will be structured in a format like: 4 | 5 | [P?] As a [user], I want to [do something], so that [value]. [FeatureCode-Priority-UserStoryNumber] 6 | Note (if any): ... 7 | 8 | Your goal is to analyze each user story and break it down into: 9 | - Test Cases under each task: Use [FeatureCode-Priority-UserStoryNumber-TestNumber]. 10 | 11 | Follow these formatting and content guidelines for your output: 12 | - Use indentation and formatting for clarity. 13 | - For Test Cases, precede the test case with the 📝 emoji. Include the functional area abbreviation (BE, FE, DE, Infra, or combinations/others like Security, Cross-team, PD) enclosed in bold brackets immediately after the emoji. Provide the test case ID immediately after the emoji, followed by the scenario description and the Expected Result. Format as: 📝 **[AREA]** [FeatureCode-Priority-UserStoryNumber-TestNumber]: [Scenario] followed by - Expected Result: [Result]. 14 | - Include both **happy path** and **edge case** test cases where relevant. 15 | - Do not invent new functionality or behaviors beyond what is described or implied by the user story and its notes. 16 | - Add a section for Non-Functional Requirements at the end of the list. Break these down into relevant tasks and test cases using the same emoji and ID formatting as functional tasks/test cases. Consider areas like Performance, Scalability, Security, Reliability, Usability, and Data Integrity based on the project context. 17 | 18 | Example Output Format (Snippet): 19 | 20 | [Feature Group Name] 21 | 22 | [P0] As a [user], I want to [action], so that [value]. [FeatureCode-Priority-UserStoryNumber] 23 | - Note (if any): ... 24 | - 📝 [FE] [FeatureCode-Priority-UserStoryNumber-Test1]: Verify component renders correctly. 25 | - Expected Result: Component is visible and interactive. 26 | - 📝 [FE] [FeatureCode-Priority-UserStoryNumber-Test2]: Test input validation for edge case. 27 | - Expected Result: System shows error for invalid input. 28 | - 📝 [BE] [FeatureCode-Priority-UserStoryNumber-Test3]: Verify data persistence for valid case. 29 | - Expected Result: Data is saved to database. 30 | F - Non-Functional Requirements 31 | 32 | [P0] As a user, I want the system to be fast. [F-P0-XX] 33 | - 📝 [Infra] [F-P0-XX-Task1-Test1]: Measure query response time under load. 34 | - Expected Result: Queries complete within X milliseconds. 35 | -------------------------------------------------------------------------------- /python_src/internal/domain/common/error.py: -------------------------------------------------------------------------------- 1 | """Domain error handling classes and utilities.""" 2 | 3 | from typing import Any, Protocol 4 | 5 | from internal.domain.common.error_code import ErrorCode 6 | 7 | 8 | class Error(Protocol): 9 | """Protocol defining the interface for domain errors.""" 10 | 11 | def __str__(self) -> str: 12 | """Return error message.""" 13 | ... 14 | 15 | def name(self) -> str: 16 | """Return error name.""" 17 | ... 18 | 19 | def client_msg(self) -> str: 20 | """Return client-facing message.""" 21 | ... 22 | 23 | 24 | class DomainError(Exception): 25 | """Domain error used for expressing errors occurring in application.""" 26 | 27 | def __init__( 28 | self, 29 | code: ErrorCode, 30 | err: Exception | None = None, 31 | client_msg: str = "", 32 | remote_status: int = 0, 33 | detail: dict[str, Any] | None = None, 34 | ): 35 | self.code = code 36 | self.err = err 37 | self._client_msg = client_msg 38 | self.remote_status = remote_status 39 | self.detail = detail or {} 40 | super().__init__(self._build_message()) 41 | 42 | def _build_message(self) -> str: 43 | """Build error message from components.""" 44 | msgs = [] 45 | 46 | if self.remote_status != 0: 47 | msgs.append(str(self.remote_status)) 48 | 49 | if self.err is not None: 50 | msgs.append(str(self.err)) 51 | 52 | if self._client_msg: 53 | msgs.append(self._client_msg) 54 | 55 | return ": ".join(msgs) 56 | 57 | def name(self) -> str: 58 | """Return error name.""" 59 | if not self.code.name: 60 | return "UNKNOWN_ERROR" 61 | return self.code.name 62 | 63 | def client_msg(self) -> str: 64 | """Return client-facing message.""" 65 | return self._client_msg 66 | 67 | def http_status(self) -> int: 68 | """Return HTTP status code.""" 69 | if self.code.status_code == 0: 70 | return 500 71 | return self.code.status_code 72 | 73 | def remote_http_status(self) -> int: 74 | """Return remote HTTP status code.""" 75 | return self.remote_status 76 | 77 | def get_detail(self) -> dict[str, Any]: 78 | """Return error detail.""" 79 | return self.detail 80 | 81 | 82 | def new_error( 83 | code: ErrorCode, 84 | err: Exception | None = None, 85 | client_msg: str = "", 86 | remote_status: int = 0, 87 | detail: dict[str, Any] | None = None, 88 | ) -> DomainError: 89 | """Create a new domain error.""" 90 | return DomainError(code=code, err=err, client_msg=client_msg, remote_status=remote_status, detail=detail) 91 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/common/error.py: -------------------------------------------------------------------------------- 1 | """Domain error handling classes and utilities.""" 2 | 3 | from typing import Any, Protocol 4 | 5 | from internal.domain.common.error_code import ErrorCode 6 | 7 | 8 | class Error(Protocol): 9 | """Protocol defining the interface for domain errors.""" 10 | 11 | def __str__(self) -> str: 12 | """Return error message.""" 13 | ... 14 | 15 | def name(self) -> str: 16 | """Return error name.""" 17 | ... 18 | 19 | def client_msg(self) -> str: 20 | """Return client-facing message.""" 21 | ... 22 | 23 | 24 | class DomainError(Exception): 25 | """Domain error used for expressing errors occurring in application.""" 26 | 27 | def __init__( 28 | self, 29 | code: ErrorCode, 30 | err: Exception | None = None, 31 | client_msg: str = "", 32 | remote_status: int = 0, 33 | detail: dict[str, Any] | None = None, 34 | ): 35 | self.code = code 36 | self.err = err 37 | self._client_msg = client_msg 38 | self.remote_status = remote_status 39 | self.detail = detail or {} 40 | super().__init__(self._build_message()) 41 | 42 | def _build_message(self) -> str: 43 | """Build error message from components.""" 44 | msgs = [] 45 | 46 | if self.remote_status != 0: 47 | msgs.append(str(self.remote_status)) 48 | 49 | if self.err is not None: 50 | msgs.append(str(self.err)) 51 | 52 | if self._client_msg: 53 | msgs.append(self._client_msg) 54 | 55 | return ": ".join(msgs) 56 | 57 | def name(self) -> str: 58 | """Return error name.""" 59 | if not self.code.name: 60 | return "UNKNOWN_ERROR" 61 | return self.code.name 62 | 63 | def client_msg(self) -> str: 64 | """Return client-facing message.""" 65 | return self._client_msg 66 | 67 | def http_status(self) -> int: 68 | """Return HTTP status code.""" 69 | if self.code.status_code == 0: 70 | return 500 71 | return self.code.status_code 72 | 73 | def remote_http_status(self) -> int: 74 | """Return remote HTTP status code.""" 75 | return self.remote_status 76 | 77 | def get_detail(self) -> dict[str, Any]: 78 | """Return error detail.""" 79 | return self.detail 80 | 81 | 82 | def new_error( 83 | code: ErrorCode, 84 | err: Exception | None = None, 85 | client_msg: str = "", 86 | remote_status: int = 0, 87 | detail: dict[str, Any] | None = None, 88 | ) -> DomainError: 89 | """Create a new domain error.""" 90 | return DomainError(code=code, err=err, client_msg=client_msg, remote_status=remote_status, detail=detail) 91 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/common/error.py: -------------------------------------------------------------------------------- 1 | """Domain error handling classes and utilities.""" 2 | 3 | from typing import Any, Protocol 4 | 5 | from internal.domain.common.error_code import ErrorCode 6 | 7 | 8 | class Error(Protocol): 9 | """Protocol defining the interface for domain errors.""" 10 | 11 | def __str__(self) -> str: 12 | """Return error message.""" 13 | ... 14 | 15 | def name(self) -> str: 16 | """Return error name.""" 17 | ... 18 | 19 | def client_msg(self) -> str: 20 | """Return client-facing message.""" 21 | ... 22 | 23 | 24 | class DomainError(Exception): 25 | """Domain error used for expressing errors occurring in application.""" 26 | 27 | def __init__( 28 | self, 29 | code: ErrorCode, 30 | err: Exception | None = None, 31 | client_msg: str = "", 32 | remote_status: int = 0, 33 | detail: dict[str, Any] | None = None, 34 | ): 35 | self.code = code 36 | self.err = err 37 | self._client_msg = client_msg 38 | self.remote_status = remote_status 39 | self.detail = detail or {} 40 | super().__init__(self._build_message()) 41 | 42 | def _build_message(self) -> str: 43 | """Build error message from components.""" 44 | msgs = [] 45 | 46 | if self.remote_status != 0: 47 | msgs.append(str(self.remote_status)) 48 | 49 | if self.err is not None: 50 | msgs.append(str(self.err)) 51 | 52 | if self._client_msg: 53 | msgs.append(self._client_msg) 54 | 55 | return ": ".join(msgs) 56 | 57 | def name(self) -> str: 58 | """Return error name.""" 59 | if not self.code.name: 60 | return "UNKNOWN_ERROR" 61 | return self.code.name 62 | 63 | def client_msg(self) -> str: 64 | """Return client-facing message.""" 65 | return self._client_msg 66 | 67 | def http_status(self) -> int: 68 | """Return HTTP status code.""" 69 | if self.code.status_code == 0: 70 | return 500 71 | return self.code.status_code 72 | 73 | def remote_http_status(self) -> int: 74 | """Return remote HTTP status code.""" 75 | return self.remote_status 76 | 77 | def get_detail(self) -> dict[str, Any]: 78 | """Return error detail.""" 79 | return self.detail 80 | 81 | 82 | def new_error( 83 | code: ErrorCode, 84 | err: Exception | None = None, 85 | client_msg: str = "", 86 | remote_status: int = 0, 87 | detail: dict[str, Any] | None = None, 88 | ) -> DomainError: 89 | """Create a new domain error.""" 90 | return DomainError(code=code, err=err, client_msg=client_msg, remote_status=remote_status, detail=detail) 91 | -------------------------------------------------------------------------------- /legacy/organization/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import pytz 4 | from django.conf import settings 5 | from django.core.validators import RegexValidator 6 | from django.db import models 7 | 8 | 9 | class Plan(models.Model): 10 | name = models.CharField(max_length=64, unique=True) 11 | is_custom = models.BooleanField(default=False) 12 | description = models.TextField(null=True, blank=True) 13 | created_at = models.DateTimeField(auto_now_add=True) 14 | updated_at = models.DateTimeField(auto_now=True) 15 | 16 | def __str__(self): 17 | return f"{self.name}" 18 | 19 | 20 | class Organization(models.Model): 21 | NAMESPACE_REGEX = "[0-9a-z_]{1,5}" # must same as url patterns 22 | 23 | name = models.CharField(max_length=255, unique=True) 24 | uuid = models.CharField(max_length=36, unique=True, default=uuid.uuid4) 25 | url_namespace = models.CharField( 26 | max_length=5, 27 | blank=True, 28 | null=True, 29 | validators=[ 30 | RegexValidator( 31 | regex=f"^{NAMESPACE_REGEX}$", 32 | message="namespace field can only contain numbers or " 33 | "lowercase letters or the bottom line", 34 | ) 35 | ], 36 | ) # deprecated 37 | plan = models.ForeignKey(Plan, on_delete=models.SET_NULL, null=True, blank=True) 38 | enable_two_factor = models.BooleanField(default=True) 39 | timezone = models.CharField( 40 | max_length=63, 41 | blank=True, 42 | null=True, 43 | default="Asia/Taipei", 44 | choices=[(tz, tz) for tz in pytz.common_timezones], 45 | help_text="Timezone for the organization, e.g., Asia/Taipei", 46 | ) 47 | language_code = models.CharField( 48 | max_length=7, 49 | default="zh-hant", 50 | choices=settings.LANGUAGES, 51 | help_text="Language code for the organization, e.g., zh-hant", 52 | ) 53 | expired_at = models.DateTimeField(null=True, blank=True) 54 | enable = models.BooleanField(default=True) 55 | 56 | def __str__(self): 57 | return f"{self.pk}:{self.name}" 58 | 59 | @property 60 | def is_new_pricing_plan(self) -> bool: 61 | return self.plan_id is not None 62 | 63 | 64 | class BusinessHour(models.Model): 65 | WEEKDAY_CHOICES = ( 66 | (1, "Monday"), 67 | (2, "Tuesday"), 68 | (3, "Wednesday"), 69 | (4, "Thursday"), 70 | (5, "Friday"), 71 | (6, "Saturday"), 72 | (7, "Sunday"), 73 | ) 74 | 75 | id = models.BigAutoField(primary_key=True) 76 | organization = models.ForeignKey(Organization, on_delete=models.CASCADE) 77 | weekday = models.PositiveSmallIntegerField(choices=WEEKDAY_CHOICES) 78 | start_time = models.TimeField() 79 | end_time = models.TimeField() 80 | -------------------------------------------------------------------------------- /.github/workflows/bot_update_pr_description.yaml: -------------------------------------------------------------------------------- 1 | name: Update PR description 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened] 6 | 7 | permissions: 8 | pull-requests: write 9 | contents: read 10 | 11 | jobs: 12 | pull-request: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Generate PR Description 17 | uses: chatbotgang/openai-pr-description@master 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | openai_api_key: ${{ secrets.OPENAI_API_KEY }} 21 | openai_model: "gpt-4.1" 22 | overwrite_description: true 23 | sample_response: | 24 | ## 🤔 Why 25 | 26 | Enables first-class support for delayed message delivery, reducing the need for external workarounds and making the system easier to use and reason about. 27 | 28 | Impact: 29 | Clients can now schedule message deliveries up to 10 minutes in the future via a simple API parameter, improving flexibility and reducing complexity. 30 | 31 | ## 💡 How 32 | 33 | - Introduced visible_at field (epoch seconds, UTC) to MessageDeliveryPlan for delayed message processing. 34 | - Enforced a maximum delay of 10 minutes (MaxVisibleAtFutureDuration) in the domain validation logic. 35 | - Updated API payloads and handlers to accept visible_at as an integer and convert to time.Time for domain logic. 36 | - Updated documentation and examples to reflect the new field and its constraints. 37 | - Ensured /delivery-plan/instant endpoint ignores visible_at and always processes immediately. 38 | - Added/updated unit and service tests to cover new validation and propagation logic. 39 | 40 | ## Check list 41 | - Asana Link: 42 | - [ ] Do you need a feature flag to protect this change? 43 | - [ ] Do you need tests to verify this change? 44 | 45 | completion_prompt: | 46 | You must follow the below template: 47 | 48 | ## 🤔 Why 49 | 50 | 53 | 54 | ## 💡 How 55 | 56 | 61 | 62 | ## Check list 63 | - Asana Link: 64 | - [ ] Do you need a feature flag to protect this change? 65 | - [ ] Do you need tests to verify this change? 66 | 67 | --- 68 | 69 | Below is the existing description of the pull request: 70 | ${{ github.event.pull_request.body }} 71 | -------------------------------------------------------------------------------- /python_src/internal/router/middleware.py: -------------------------------------------------------------------------------- 1 | """HTTP middleware for request processing.""" 2 | 3 | import time 4 | from collections.abc import Callable 5 | 6 | import structlog 7 | from fastapi import Request, Response 8 | from starlette.middleware.base import BaseHTTPMiddleware 9 | from starlette.types import ASGIApp 10 | 11 | from internal.domain.common.requestid import get_request_id, new_request_id, set_request_id 12 | 13 | 14 | class RequestIDMiddleware(BaseHTTPMiddleware): 15 | """Middleware for managing request IDs.""" 16 | 17 | HEADER_X_REQUEST_ID = "X-Request-ID" 18 | 19 | async def dispatch(self, request: Request, call_next: Callable) -> Response: 20 | # Get or generate request ID 21 | request_id = request.headers.get(self.HEADER_X_REQUEST_ID, "") 22 | if not request_id: 23 | request_id = new_request_id() 24 | 25 | # Set request ID in context 26 | set_request_id(request_id) 27 | 28 | # Process request 29 | response = await call_next(request) 30 | 31 | # Add request ID to response headers 32 | response.headers[self.HEADER_X_REQUEST_ID] = request_id 33 | 34 | return response 35 | 36 | 37 | class LoggingMiddleware(BaseHTTPMiddleware): 38 | """Middleware for request/response logging.""" 39 | 40 | def __init__(self, app: ASGIApp, logger: structlog.BoundLogger): 41 | super().__init__(app) 42 | self.logger = logger 43 | 44 | async def dispatch(self, request: Request, call_next: Callable) -> Response: 45 | start_time = time.time() 46 | 47 | # Skip health check to avoid polluting logs 48 | if request.url.path == "/api/v1/health": 49 | return await call_next(request) 50 | 51 | # Get request ID for logging context 52 | request_id = get_request_id() 53 | 54 | # Create request logger with context 55 | log = self.logger.bind(requestID=request_id, path=request.url.path, method=request.method, component="router") 56 | 57 | try: 58 | # Process request 59 | response = await call_next(request) 60 | 61 | # Calculate latency 62 | latency = time.time() - start_time 63 | 64 | # Log successful request 65 | log.info( 66 | "Request processed", 67 | status=response.status_code, 68 | latency=f"{latency:.6f}s", 69 | clientIP=request.client.host if request.client else "unknown", 70 | fullPath=str(request.url), 71 | userAgent=request.headers.get("User-Agent", ""), 72 | ) 73 | 74 | return response 75 | 76 | except Exception as e: 77 | # Calculate latency 78 | latency = time.time() - start_time 79 | 80 | # Log error 81 | log.error( 82 | "Request failed", 83 | error=str(e), 84 | latency=f"{latency:.6f}s", 85 | clientIP=request.client.host if request.client else "unknown", 86 | fullPath=str(request.url), 87 | userAgent=request.headers.get("User-Agent", ""), 88 | ) 89 | 90 | raise 91 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/router/middleware.py: -------------------------------------------------------------------------------- 1 | """HTTP middleware for request processing.""" 2 | 3 | import time 4 | from collections.abc import Callable 5 | 6 | import structlog 7 | from fastapi import Request, Response 8 | from starlette.middleware.base import BaseHTTPMiddleware 9 | from starlette.types import ASGIApp 10 | 11 | from internal.domain.common.requestid import get_request_id, new_request_id, set_request_id 12 | 13 | 14 | class RequestIDMiddleware(BaseHTTPMiddleware): 15 | """Middleware for managing request IDs.""" 16 | 17 | HEADER_X_REQUEST_ID = "X-Request-ID" 18 | 19 | async def dispatch(self, request: Request, call_next: Callable) -> Response: 20 | # Get or generate request ID 21 | request_id = request.headers.get(self.HEADER_X_REQUEST_ID, "") 22 | if not request_id: 23 | request_id = new_request_id() 24 | 25 | # Set request ID in context 26 | set_request_id(request_id) 27 | 28 | # Process request 29 | response = await call_next(request) 30 | 31 | # Add request ID to response headers 32 | response.headers[self.HEADER_X_REQUEST_ID] = request_id 33 | 34 | return response 35 | 36 | 37 | class LoggingMiddleware(BaseHTTPMiddleware): 38 | """Middleware for request/response logging.""" 39 | 40 | def __init__(self, app: ASGIApp, logger: structlog.BoundLogger): 41 | super().__init__(app) 42 | self.logger = logger 43 | 44 | async def dispatch(self, request: Request, call_next: Callable) -> Response: 45 | start_time = time.time() 46 | 47 | # Skip health check to avoid polluting logs 48 | if request.url.path == "/api/v1/health": 49 | return await call_next(request) 50 | 51 | # Get request ID for logging context 52 | request_id = get_request_id() 53 | 54 | # Create request logger with context 55 | log = self.logger.bind(requestID=request_id, path=request.url.path, method=request.method, component="router") 56 | 57 | try: 58 | # Process request 59 | response = await call_next(request) 60 | 61 | # Calculate latency 62 | latency = time.time() - start_time 63 | 64 | # Log successful request 65 | log.info( 66 | "Request processed", 67 | status=response.status_code, 68 | latency=f"{latency:.6f}s", 69 | clientIP=request.client.host if request.client else "unknown", 70 | fullPath=str(request.url), 71 | userAgent=request.headers.get("User-Agent", ""), 72 | ) 73 | 74 | return response 75 | 76 | except Exception as e: 77 | # Calculate latency 78 | latency = time.time() - start_time 79 | 80 | # Log error 81 | log.error( 82 | "Request failed", 83 | error=str(e), 84 | latency=f"{latency:.6f}s", 85 | clientIP=request.client.host if request.client else "unknown", 86 | fullPath=str(request.url), 87 | userAgent=request.headers.get("User-Agent", ""), 88 | ) 89 | 90 | raise 91 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/router/middleware.py: -------------------------------------------------------------------------------- 1 | """HTTP middleware for request processing.""" 2 | 3 | import time 4 | from collections.abc import Callable 5 | 6 | import structlog 7 | from fastapi import Request, Response 8 | from starlette.middleware.base import BaseHTTPMiddleware 9 | from starlette.types import ASGIApp 10 | 11 | from internal.domain.common.requestid import get_request_id, new_request_id, set_request_id 12 | 13 | 14 | class RequestIDMiddleware(BaseHTTPMiddleware): 15 | """Middleware for managing request IDs.""" 16 | 17 | HEADER_X_REQUEST_ID = "X-Request-ID" 18 | 19 | async def dispatch(self, request: Request, call_next: Callable) -> Response: 20 | # Get or generate request ID 21 | request_id = request.headers.get(self.HEADER_X_REQUEST_ID, "") 22 | if not request_id: 23 | request_id = new_request_id() 24 | 25 | # Set request ID in context 26 | set_request_id(request_id) 27 | 28 | # Process request 29 | response = await call_next(request) 30 | 31 | # Add request ID to response headers 32 | response.headers[self.HEADER_X_REQUEST_ID] = request_id 33 | 34 | return response 35 | 36 | 37 | class LoggingMiddleware(BaseHTTPMiddleware): 38 | """Middleware for request/response logging.""" 39 | 40 | def __init__(self, app: ASGIApp, logger: structlog.BoundLogger): 41 | super().__init__(app) 42 | self.logger = logger 43 | 44 | async def dispatch(self, request: Request, call_next: Callable) -> Response: 45 | start_time = time.time() 46 | 47 | # Skip health check to avoid polluting logs 48 | if request.url.path == "/api/v1/health": 49 | return await call_next(request) 50 | 51 | # Get request ID for logging context 52 | request_id = get_request_id() 53 | 54 | # Create request logger with context 55 | log = self.logger.bind(requestID=request_id, path=request.url.path, method=request.method, component="router") 56 | 57 | try: 58 | # Process request 59 | response = await call_next(request) 60 | 61 | # Calculate latency 62 | latency = time.time() - start_time 63 | 64 | # Log successful request 65 | log.info( 66 | "Request processed", 67 | status=response.status_code, 68 | latency=f"{latency:.6f}s", 69 | clientIP=request.client.host if request.client else "unknown", 70 | fullPath=str(request.url), 71 | userAgent=request.headers.get("User-Agent", ""), 72 | ) 73 | 74 | return response 75 | 76 | except Exception as e: 77 | # Calculate latency 78 | latency = time.time() - start_time 79 | 80 | # Log error 81 | log.error( 82 | "Request failed", 83 | error=str(e), 84 | latency=f"{latency:.6f}s", 85 | clientIP=request.client.host if request.client else "unknown", 86 | fullPath=str(request.url), 87 | userAgent=request.headers.get("User-Agent", ""), 88 | ) 89 | 90 | raise 91 | -------------------------------------------------------------------------------- /legacy/line/services/__init__.py: -------------------------------------------------------------------------------- 1 | from line.services.bot import ( 2 | BuildLineWebhookAndForwardService, 3 | CheckLineMessageLimitService, 4 | CheckLineWebhookForwardHealthService, 5 | ForwardLineWebhookService, 6 | VerifyBotChannelService, 7 | ) 8 | from line.services.broadcast import GetBroadcastReportService 9 | from line.services.deep_link import ( 10 | BatchCreateDeepLinkService, 11 | BatchUpdateDeepLinkService, 12 | ExportDeepLinkQRCodeService, 13 | ExportDeepLinkSettingService, 14 | GetDeepLinkFileNameService, 15 | SearchDeepLinkService, 16 | ValidateCreateDeepLinkFileService, 17 | ValidateDeepLinkIdService, 18 | ValidateUpdateDeepLinkFileService, 19 | ) 20 | from line.services.login import CreateLoginService, VerifyLoginChannelService 21 | from line.services.member import ( 22 | GetMemberService, 23 | ImportMemberFromFileService, 24 | UpdateMemberService, 25 | UpdateOrCreateMemberService, 26 | UploadImportingFileRequestService, 27 | ) 28 | from line.services.message import BuildMessageAndValidateService 29 | from line.services.message_link import ClickMessageLinkService 30 | from line.services.rich_menu import LinkTagEventRichMenusToMembersService 31 | from line.services.share_link import ( 32 | BuildShareMessagesService, 33 | CreateShareLinkRecordService, 34 | GetShareLinkService, 35 | ) 36 | from line.services.sms_plus import ( 37 | BuildQueryFilterPNPMessageRecordService, 38 | BuildQueryFilterPNPMessageSettingService, 39 | CountPNPMessageRecordStatusService, 40 | ) 41 | from line.services.trace_link import GetTraceLinkReportService, GetTraceLinkService 42 | from line.services.webhook import ProcessLineWebhookService 43 | from line.services.webhook_trigger import GetMonthlyScheduleService 44 | from organization.services import CreateJsonNotificationService 45 | 46 | __all__ = [ 47 | "CheckLineMessageLimitService", 48 | "VerifyBotChannelService", 49 | "CreateLoginService", 50 | "VerifyLoginChannelService", 51 | "GetBroadcastReportService", 52 | "BatchCreateDeepLinkService", 53 | "CreateJsonNotificationService", 54 | "ValidateCreateDeepLinkFileService", 55 | "ValidateUpdateDeepLinkFileService", 56 | "GetMemberService", 57 | "SearchDeepLinkService", 58 | "ValidateDeepLinkIdService", 59 | "ExportDeepLinkSettingService", 60 | "BatchUpdateDeepLinkService", 61 | "ExportDeepLinkQRCodeService", 62 | "UploadImportingFileRequestService", 63 | "ImportMemberFromFileService", 64 | "GetDeepLinkFileNameService", 65 | "ClickMessageLinkService", 66 | "GetTraceLinkReportService", 67 | "GetTraceLinkService", 68 | "BuildShareMessagesService", 69 | "CreateShareLinkRecordService", 70 | "GetShareLinkService", 71 | "BuildQueryFilterPNPMessageRecordService", 72 | "CountPNPMessageRecordStatusService", 73 | "BuildQueryFilterPNPMessageSettingService", 74 | "UpdateOrCreateMemberService", 75 | "LinkTagEventRichMenusToMembersService", 76 | "UpdateMemberService", 77 | "BuildMessageAndValidateService", 78 | "ForwardLineWebhookService", 79 | "CheckLineWebhookForwardHealthService", 80 | "BuildLineWebhookAndForwardService", 81 | "ProcessLineWebhookService", 82 | "GetMonthlyScheduleService", 83 | ] 84 | -------------------------------------------------------------------------------- /go_src/cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/alecthomas/kingpin/v2" 13 | "github.com/rs/zerolog" 14 | 15 | "github.com/chatbotgang/workshop/internal/app" 16 | ) 17 | 18 | var ( 19 | AppName = "workshop" 20 | AppVersion = "unknown_version" 21 | AppBuild = "unknown_build" 22 | ) 23 | 24 | const ( 25 | defaultEnv = "staging" 26 | defaultLogLevel = "info" 27 | defaultPort = "8080" 28 | ) 29 | 30 | type AppConfig struct { 31 | // General configuration 32 | Env *string 33 | LogLevel *string 34 | 35 | // HTTP configuration 36 | Port *int 37 | } 38 | 39 | func initAppConfig() AppConfig { 40 | // Setup basic application information 41 | app := kingpin.New(AppName, "The HTTP server"). 42 | Version(fmt.Sprintf("version: %s, build: %s", AppVersion, AppBuild)) 43 | 44 | var config AppConfig 45 | 46 | config.Env = app. 47 | Flag("env", "The running environment"). 48 | Envar("WORKSHOP_ENV").Default(defaultEnv).Enum("local", "staging", "production") 49 | 50 | config.LogLevel = app. 51 | Flag("log_level", "Log filtering level"). 52 | Envar("WORKSHOP_LOG_LEVEL").Default(defaultLogLevel).Enum("error", "warn", "info", "debug", "disabled") 53 | 54 | config.Port = app. 55 | Flag("port", "The HTTP server port"). 56 | Envar("WORKSHOP_PORT").Default(defaultPort).Int() 57 | 58 | kingpin.MustParse(app.Parse(os.Args[1:])) 59 | 60 | return config 61 | } 62 | 63 | func initRootLogger(levelStr, env string) zerolog.Logger { 64 | // Set global log level 65 | level, err := zerolog.ParseLevel(levelStr) 66 | if err != nil { 67 | level = zerolog.InfoLevel 68 | } 69 | zerolog.SetGlobalLevel(level) 70 | 71 | // Set logger time format 72 | const rfc3339Micro = "2006-01-02T15:04:05.000000Z07:00" 73 | zerolog.TimeFieldFormat = rfc3339Micro 74 | zerolog.DurationFieldUnit = time.Second 75 | 76 | rootLogger := zerolog.New(os.Stdout).With(). 77 | Timestamp(). 78 | Str("service", AppName). 79 | Str("env", env). 80 | Logger() 81 | 82 | return rootLogger 83 | } 84 | 85 | func main() { 86 | 87 | // Setup app configuration 88 | cfg := initAppConfig() 89 | 90 | // Create root logger 91 | rootLogger := initRootLogger(*cfg.LogLevel, *cfg.Env) 92 | 93 | // Create root context 94 | rootCtx, rootCtxCancelFunc := context.WithCancel(context.Background()) 95 | rootCtx = rootLogger.WithContext(rootCtx) 96 | 97 | rootLogger.Info(). 98 | Str("version", AppVersion). 99 | Str("build", AppBuild). 100 | Msgf("Launching %s", AppName) 101 | 102 | wg := sync.WaitGroup{} 103 | // Create application 104 | app := app.MustNewApplication(rootCtx, &wg, app.ApplicationParam{ 105 | AppName: AppName, 106 | Env: *cfg.Env, 107 | }) 108 | 109 | // Run server 110 | wg.Add(1) 111 | runHTTPServer(rootCtx, &wg, *cfg.Port, app) 112 | 113 | // Listen to SIGTERM/SIGINT to close 114 | var gracefulStop = make(chan os.Signal, 1) 115 | signal.Notify(gracefulStop, syscall.SIGTERM, syscall.SIGINT) 116 | <-gracefulStop 117 | rootCtxCancelFunc() 118 | 119 | // Wait for all services to close with a specific timeout 120 | var waitUntilDone = make(chan struct{}) 121 | go func() { 122 | wg.Wait() 123 | close(waitUntilDone) 124 | }() 125 | select { 126 | case <-waitUntilDone: 127 | rootLogger.Info().Msg("success to close all services") 128 | case <-time.After(10 * time.Second): 129 | rootLogger.Err(context.DeadlineExceeded).Msg("fail to close all services") 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/auto_reply/webhook_event.py: -------------------------------------------------------------------------------- 1 | """Webhook Event domain models.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from datetime import datetime 5 | from enum import StrEnum 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | from internal.domain.auto_reply.webhook_trigger import WebhookTriggerEventType 10 | 11 | 12 | class WebhookEventType(StrEnum): 13 | """Webhook event type enumeration.""" 14 | 15 | MESSAGE = "message" 16 | POSTBACK = "postback" 17 | FOLLOW = "follow" 18 | BEACON = "beacon" 19 | 20 | 21 | class ChannelType(StrEnum): 22 | """Channel type enumeration.""" 23 | 24 | LINE = "line" 25 | FACEBOOK = "facebook" 26 | INSTAGRAM = "instagram" 27 | 28 | 29 | class WebhookEvent(BaseModel, ABC): 30 | """Abstract base class for webhook events.""" 31 | 32 | event_id: str = Field(..., description="Unique event identifier") 33 | channel_type: ChannelType = Field(..., description="Channel where event originated") 34 | user_id: str = Field(..., description="User identifier from the channel") 35 | timestamp: datetime = Field(..., description="Event timestamp") 36 | 37 | @abstractmethod 38 | def get_event_type(self) -> WebhookEventType: 39 | """Get the event type.""" 40 | pass 41 | 42 | @abstractmethod 43 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 44 | """Get the corresponding trigger event type for matching.""" 45 | pass 46 | 47 | class Config: 48 | """Pydantic configuration.""" 49 | 50 | use_enum_values = True 51 | 52 | 53 | class MessageEvent(WebhookEvent): 54 | """Message webhook event.""" 55 | 56 | content: str = Field(..., description="Message content/text") 57 | message_id: str = Field(..., description="Unique message identifier") 58 | 59 | def get_event_type(self) -> WebhookEventType: 60 | """Get the event type.""" 61 | return WebhookEventType.MESSAGE 62 | 63 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 64 | """Get the corresponding trigger event type for matching.""" 65 | return WebhookTriggerEventType.MESSAGE 66 | 67 | def get_normalized_content(self) -> str: 68 | """Get normalized message content for keyword matching.""" 69 | return self.content.strip().lower() 70 | 71 | 72 | class PostbackEvent(WebhookEvent): 73 | """Postback webhook event.""" 74 | 75 | data: str = Field(..., description="Postback data/payload") 76 | postback_id: str = Field(..., description="Unique postback identifier") 77 | 78 | def get_event_type(self) -> WebhookEventType: 79 | """Get the event type.""" 80 | return WebhookEventType.POSTBACK 81 | 82 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 83 | """Get the corresponding trigger event type for matching.""" 84 | return WebhookTriggerEventType.POSTBACK 85 | 86 | 87 | class FollowEvent(WebhookEvent): 88 | """Follow webhook event.""" 89 | 90 | def get_event_type(self) -> WebhookEventType: 91 | """Get the event type.""" 92 | return WebhookEventType.FOLLOW 93 | 94 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 95 | """Get the corresponding trigger event type for matching.""" 96 | return WebhookTriggerEventType.FOLLOW 97 | 98 | 99 | class BeaconEvent(WebhookEvent): 100 | """Beacon webhook event.""" 101 | 102 | beacon_data: dict[str, object] = Field(..., description="Beacon event data") 103 | 104 | def get_event_type(self) -> WebhookEventType: 105 | """Get the event type.""" 106 | return WebhookEventType.BEACON 107 | 108 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 109 | """Get the corresponding trigger event type for matching.""" 110 | return WebhookTriggerEventType.BEACON 111 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/internal/domain/auto_reply/webhook_event.py: -------------------------------------------------------------------------------- 1 | """Webhook Event domain models.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from datetime import datetime 5 | from enum import StrEnum 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | from internal.domain.auto_reply.webhook_trigger import WebhookTriggerEventType 10 | 11 | 12 | class WebhookEventType(StrEnum): 13 | """Webhook event type enumeration.""" 14 | 15 | MESSAGE = "message" 16 | POSTBACK = "postback" 17 | FOLLOW = "follow" 18 | BEACON = "beacon" 19 | 20 | 21 | class ChannelType(StrEnum): 22 | """Channel type enumeration.""" 23 | 24 | LINE = "line" 25 | FACEBOOK = "facebook" 26 | INSTAGRAM = "instagram" 27 | 28 | 29 | class WebhookEvent(BaseModel, ABC): 30 | """Abstract base class for webhook events.""" 31 | 32 | event_id: str = Field(..., description="Unique event identifier") 33 | channel_type: ChannelType = Field(..., description="Channel where event originated") 34 | user_id: str = Field(..., description="User identifier from the channel") 35 | timestamp: datetime = Field(..., description="Event timestamp") 36 | extra: dict[str, object] | None = Field(default=None, description="Additional platform-specific metadata") 37 | 38 | @abstractmethod 39 | def get_event_type(self) -> WebhookEventType: 40 | """Get the event type.""" 41 | pass 42 | 43 | @abstractmethod 44 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 45 | """Get the corresponding trigger event type for matching.""" 46 | pass 47 | 48 | class Config: 49 | """Pydantic configuration.""" 50 | 51 | use_enum_values = True 52 | 53 | 54 | class MessageEvent(WebhookEvent): 55 | """Message webhook event.""" 56 | 57 | content: str = Field(..., description="Message content/text") 58 | message_id: str = Field(..., description="Unique message identifier") 59 | 60 | def get_event_type(self) -> WebhookEventType: 61 | """Get the event type.""" 62 | return WebhookEventType.MESSAGE 63 | 64 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 65 | """Get the corresponding trigger event type for matching.""" 66 | return WebhookTriggerEventType.MESSAGE 67 | 68 | def get_normalized_content(self) -> str: 69 | """Get normalized message content for keyword matching.""" 70 | return self.content.strip().lower() 71 | 72 | 73 | class PostbackEvent(WebhookEvent): 74 | """Postback webhook event.""" 75 | 76 | data: str = Field(..., description="Postback data/payload") 77 | postback_id: str = Field(..., description="Unique postback identifier") 78 | 79 | def get_event_type(self) -> WebhookEventType: 80 | """Get the event type.""" 81 | return WebhookEventType.POSTBACK 82 | 83 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 84 | """Get the corresponding trigger event type for matching.""" 85 | return WebhookTriggerEventType.POSTBACK 86 | 87 | 88 | class FollowEvent(WebhookEvent): 89 | """Follow webhook event.""" 90 | 91 | def get_event_type(self) -> WebhookEventType: 92 | """Get the event type.""" 93 | return WebhookEventType.FOLLOW 94 | 95 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 96 | """Get the corresponding trigger event type for matching.""" 97 | return WebhookTriggerEventType.FOLLOW 98 | 99 | 100 | class BeaconEvent(WebhookEvent): 101 | """Beacon webhook event.""" 102 | 103 | beacon_data: dict[str, object] = Field(..., description="Beacon event data") 104 | 105 | def get_event_type(self) -> WebhookEventType: 106 | """Get the event type.""" 107 | return WebhookEventType.BEACON 108 | 109 | def get_trigger_event_type(self) -> WebhookTriggerEventType: 110 | """Get the corresponding trigger event type for matching.""" 111 | return WebhookTriggerEventType.BEACON 112 | -------------------------------------------------------------------------------- /go_src/internal/router/middleware_logger.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "io" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/rs/zerolog" 13 | 14 | "github.com/chatbotgang/workshop/internal/domain/common" 15 | ) 16 | 17 | var sensitiveAPIs = map[string]bool{} 18 | 19 | // filterSensitiveAPI only returns `email` field for sensitive APIs 20 | func filterSensitiveAPI(path string, data []byte) []byte { 21 | type email struct { 22 | Email string `json:"email"` 23 | } 24 | 25 | _, ok := sensitiveAPIs[path] 26 | if ok { 27 | var e email 28 | err := json.Unmarshal(data, &e) 29 | if err != nil || e.Email == "" { 30 | return []byte{} 31 | } 32 | 33 | ret, err := json.Marshal(e) 34 | if err != nil { 35 | return []byte{} 36 | } 37 | 38 | return ret 39 | } 40 | 41 | return data 42 | } 43 | 44 | type loggerParams struct { 45 | clientIP string 46 | statusCode int 47 | bodySize int 48 | userAgent string 49 | errorMessage string 50 | fullPath string 51 | } 52 | 53 | func buildLoggerParam(c *gin.Context) loggerParams { 54 | params := loggerParams{ 55 | clientIP: c.ClientIP(), 56 | statusCode: c.Writer.Status(), 57 | bodySize: c.Writer.Size(), 58 | userAgent: c.Request.Header.Get("User-Agent"), 59 | } 60 | 61 | // collect error message 62 | if err := c.Errors.Last(); err != nil { 63 | params.errorMessage = err.Error() 64 | } 65 | 66 | // collect full path 67 | path := c.Request.URL.Path 68 | raw := c.Request.URL.RawQuery 69 | if raw != "" { 70 | path = path + "?" + raw 71 | } 72 | params.fullPath = path 73 | 74 | return params 75 | } 76 | 77 | // LoggerMiddleware is referenced from gin's logger implementation with additional capabilities: 78 | // 1. use zerolog to do structure log 79 | // 2. add requestID into context logger 80 | func LoggerMiddleware(rootCtx context.Context) gin.HandlerFunc { 81 | return func(c *gin.Context) { 82 | // Start timer 83 | start := time.Now() 84 | 85 | // Ignore health-check to avoid polluting API logs 86 | path := c.Request.URL.Path 87 | if path == "/api/v1/health" { 88 | c.Next() 89 | return 90 | } 91 | 92 | // Add RequestID into the logger of the request context 93 | requestID := common.GetRequestID(c.Request.Context()) 94 | zlog := zerolog.Ctx(rootCtx).With(). 95 | Str("requestID", requestID). 96 | Str("path", c.FullPath()). 97 | Str("method", c.Request.Method). 98 | Logger() 99 | c.Request = c.Request.WithContext(zlog.WithContext(context.WithoutCancel(c.Request.Context()))) 100 | 101 | // Use TeeReader to duplicate the request body to an internal buffer, so 102 | // that we could use it for logging 103 | var buf bytes.Buffer 104 | tee := io.TeeReader(c.Request.Body, &buf) 105 | c.Request.Body = io.NopCloser(tee) 106 | 107 | // Process request 108 | c.Next() 109 | 110 | // Build all information that we want to log 111 | params := buildLoggerParam(c) 112 | 113 | // Build logger with proper severity 114 | var l *zerolog.Event 115 | if params.statusCode >= 300 || len(params.errorMessage) != 0 { 116 | l = zerolog.Ctx(c.Request.Context()).Error() 117 | } else { 118 | l = zerolog.Ctx(c.Request.Context()).Info() 119 | } 120 | 121 | l = l.Time("callTime", start). 122 | Int("status", params.statusCode). 123 | Dur("latency", time.Since(start)). 124 | Str("clientIP", params.clientIP). 125 | Str("fullPath", params.fullPath). 126 | Str("component", "router"). 127 | Str("userAgent", params.userAgent) 128 | 129 | if params.errorMessage != "" { 130 | l = l.Err(errors.New(params.errorMessage)) 131 | } 132 | if buf.Len() > 0 { 133 | data := buf.Bytes() 134 | 135 | // Try to filter request body if it's a sensitive API 136 | data = filterSensitiveAPI(params.fullPath, data) 137 | 138 | var jsonBuf bytes.Buffer 139 | if err := json.Compact(&jsonBuf, data); err == nil { 140 | l = l.RawJSON("request", jsonBuf.Bytes()) 141 | } 142 | } 143 | 144 | l.Send() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /legacy/line/webhook/base.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from line.domains import Bot 4 | from organization.domains.organization import Organization 5 | from packages.line.domains.event import ( 6 | AccountLinkEvent, 7 | BeaconEvent, 8 | Event, 9 | FollowEvent, 10 | JoinEvent, 11 | LeaveEvent, 12 | MemberJoinedEvent, 13 | MemberLeftEvent, 14 | MessageEvent, 15 | PostbackEvent, 16 | UnfollowEvent, 17 | UnsendEvent, 18 | VideoPlayCompleteEvent, 19 | ) 20 | 21 | 22 | class BaseWebhookHandler: 23 | TYPE_MAP = { 24 | "message": "message", 25 | "unsend": "unsend", 26 | "follow": "follow", 27 | "unfollow": "unfollow", 28 | "join": "join", 29 | "leave": "leave", 30 | "memberJoined": "member_joined", 31 | "memberLeft": "member_left", 32 | "postback": "postback", 33 | "beacon": "beacon", 34 | "accountLink": "account_link", 35 | "things": "things", 36 | "delivery": "delivery", 37 | } 38 | 39 | def __init__( 40 | self, 41 | bot: dict, 42 | event: dict, 43 | uuid: str, 44 | bot_instance: Optional[Bot] = None, 45 | event_instance: Optional[ 46 | Union[ 47 | Event, 48 | MessageEvent, 49 | UnsendEvent, 50 | FollowEvent, 51 | UnfollowEvent, 52 | JoinEvent, 53 | LeaveEvent, 54 | MemberJoinedEvent, 55 | MemberLeftEvent, 56 | PostbackEvent, 57 | VideoPlayCompleteEvent, 58 | BeaconEvent, 59 | AccountLinkEvent, 60 | ] 61 | ] = None, 62 | organization: Optional[Organization] = None, 63 | ): 64 | # TODO: implement Singleton pattern in webhook handlers 65 | self.bot = bot 66 | self.event = event 67 | self.bot_instance = bot_instance 68 | self.event_instance = event_instance 69 | self.organization = organization 70 | self.uuid = uuid 71 | self.mode = event.get("mode", "active") 72 | 73 | @property 74 | def is_channel_active(self) -> bool: 75 | # Channel state 76 | # "active": The channel is active. You can send a reply message or 77 | # push message, etc. from the bot server that received this webhook event. 78 | # "standby": The channel is waiting. When the channel state is standby, 79 | # the webhook event won't contain a reply token to send reply message. 80 | # Ref: https://developers.line.biz/en/reference/messaging-api/#common-properties 81 | return self.mode == "active" 82 | 83 | def message(self, process_data: dict) -> dict: 84 | return process_data 85 | 86 | def unsend(self, process_data: dict) -> dict: 87 | return process_data 88 | 89 | def follow(self, process_data: dict) -> dict: 90 | return process_data 91 | 92 | def unfollow(self, process_data: dict) -> dict: 93 | return process_data 94 | 95 | def join(self, process_data: dict) -> dict: 96 | return process_data 97 | 98 | def leave(self, process_data: dict) -> dict: 99 | return process_data 100 | 101 | def member_joined(self, process_data: dict) -> dict: 102 | return process_data 103 | 104 | def member_left(self, process_data: dict) -> dict: 105 | return process_data 106 | 107 | def postback(self, process_data: dict) -> dict: 108 | return process_data 109 | 110 | def beacon(self, process_data: dict) -> dict: 111 | return process_data 112 | 113 | def account_link(self, process_data: dict) -> dict: 114 | return process_data 115 | 116 | def things(self, process_data: dict) -> dict: 117 | return process_data 118 | 119 | def delivery(self, process_data: dict) -> dict: 120 | return process_data 121 | 122 | def add_messages( 123 | self, 124 | process_data: dict, 125 | messages: list, 126 | type: str, 127 | ref_id: int, 128 | insert: bool = False, 129 | ) -> dict: 130 | message_infos = [] 131 | if insert: 132 | process_data["messages"] = messages.extend(process_data["messages"]) 133 | for i in range(len(messages)): 134 | message_infos.append({"type": type, "ref_id": ref_id}) 135 | process_data["message_infos"] = message_infos.extend( 136 | process_data["message_infos"] 137 | ) 138 | else: 139 | process_data["messages"].extend(messages) 140 | for i in range(len(messages)): 141 | message_infos.append({"type": type, "ref_id": ref_id}) 142 | process_data["message_infos"].extend(message_infos) 143 | return process_data 144 | -------------------------------------------------------------------------------- /python_src/internal/domain/auto_reply/webhook_trigger.py: -------------------------------------------------------------------------------- 1 | """Webhook Trigger domain models.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from datetime import datetime 5 | from enum import IntEnum, StrEnum 6 | 7 | from pydantic import BaseModel 8 | 9 | 10 | class WebhookTriggerEventType(IntEnum): 11 | """Webhook trigger event type enumeration.""" 12 | 13 | MESSAGE = 1 14 | POSTBACK = 2 15 | FOLLOW = 3 16 | BEACON = 4 17 | TIME = 100 18 | MESSAGE_EDITOR = 101 19 | POSTBACK_EDITOR = 102 20 | 21 | 22 | class WebhookTriggerScheduleType(StrEnum): 23 | """Webhook trigger schedule type enumeration.""" 24 | 25 | DAILY = "daily" 26 | BUSINESS_HOUR = "business_hour" 27 | NON_BUSINESS_HOUR = "non_business_hour" 28 | MONTHLY = "monthly" 29 | DATE_RANGE = "date_range" 30 | 31 | 32 | class WebhookTriggerSchedule(BaseModel, ABC): 33 | """Abstract base class for webhook trigger schedule.""" 34 | 35 | @abstractmethod 36 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 37 | """Get the schedule type.""" 38 | pass 39 | 40 | @abstractmethod 41 | def get_schedule_settings(self) -> dict[str, object] | None: 42 | """Get the schedule settings.""" 43 | pass 44 | 45 | 46 | class DailySchedule(WebhookTriggerSchedule): 47 | """Daily trigger schedule.""" 48 | 49 | start_time: str 50 | end_time: str 51 | 52 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 53 | """Get the schedule type.""" 54 | return WebhookTriggerScheduleType.DAILY 55 | 56 | def get_schedule_settings(self) -> dict[str, object]: 57 | """Get the schedule settings.""" 58 | return {"start_time": self.start_time, "end_time": self.end_time} 59 | 60 | 61 | class MonthlySchedule(WebhookTriggerSchedule): 62 | """Monthly trigger schedule.""" 63 | 64 | day: int 65 | start_time: str 66 | end_time: str 67 | 68 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 69 | """Get the schedule type.""" 70 | return WebhookTriggerScheduleType.MONTHLY 71 | 72 | def get_schedule_settings(self) -> dict[str, object]: 73 | """Get the schedule settings.""" 74 | return {"day": self.day, "start_time": self.start_time, "end_time": self.end_time} 75 | 76 | 77 | class DateRangeSchedule(WebhookTriggerSchedule): 78 | """Date range trigger schedule.""" 79 | 80 | start_date: str 81 | end_date: str 82 | 83 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 84 | """Get the schedule type.""" 85 | return WebhookTriggerScheduleType.DATE_RANGE 86 | 87 | def get_schedule_settings(self) -> dict[str, object]: 88 | """Get the schedule settings.""" 89 | return {"start_date": self.start_date, "end_date": self.end_date} 90 | 91 | 92 | class BusinessHourSchedule(WebhookTriggerSchedule): 93 | """Business hour trigger schedule.""" 94 | 95 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 96 | """Get the schedule type.""" 97 | return WebhookTriggerScheduleType.BUSINESS_HOUR 98 | 99 | def get_schedule_settings(self) -> dict[str, object] | None: 100 | """Get the schedule settings.""" 101 | return None 102 | 103 | 104 | class NonBusinessHourSchedule(WebhookTriggerSchedule): 105 | """Non-business hour trigger schedule.""" 106 | 107 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 108 | """Get the schedule type.""" 109 | return WebhookTriggerScheduleType.NON_BUSINESS_HOUR 110 | 111 | def get_schedule_settings(self) -> dict[str, object] | None: 112 | """Get the schedule settings.""" 113 | return None 114 | 115 | 116 | class WebhookTriggerScheduleSettings(BaseModel): 117 | """Webhook trigger schedule settings.""" 118 | 119 | schedules: list[ 120 | DailySchedule | MonthlySchedule | DateRangeSchedule | BusinessHourSchedule | NonBusinessHourSchedule 121 | ] 122 | 123 | class Config: 124 | """Pydantic configuration.""" 125 | 126 | use_enum_values = True 127 | 128 | 129 | class WebhookTriggerSetting(BaseModel): 130 | """Webhook trigger setting domain model. 131 | 132 | Represents the channel-level configuration for webhook triggers (Auto-Reply). 133 | """ 134 | 135 | id: int 136 | auto_reply_id: int 137 | bot_id: int 138 | enable: bool 139 | name: str # Will be deprecated 140 | event_type: WebhookTriggerEventType 141 | trigger_code: str | None = None # Will be deprecated 142 | trigger_schedule_type: WebhookTriggerScheduleType | None = None # Will be deprecated 143 | trigger_schedule_settings: dict[str, object] | None = None # Will be deprecated 144 | created_at: datetime 145 | updated_at: datetime 146 | archived: bool = False 147 | extra: dict[str, object] | None = None 148 | 149 | def is_active(self) -> bool: 150 | """Check if the webhook trigger setting is active.""" 151 | return self.enable and not self.archived 152 | 153 | class Config: 154 | """Pydantic configuration.""" 155 | 156 | use_enum_values = True 157 | -------------------------------------------------------------------------------- /cheat_sheet/python/1_rewrite_brownfield/internal/domain/auto_reply/webhook_trigger.py: -------------------------------------------------------------------------------- 1 | """Webhook Trigger domain models.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from datetime import datetime 5 | from enum import IntEnum, StrEnum 6 | 7 | from pydantic import BaseModel 8 | 9 | 10 | class WebhookTriggerEventType(IntEnum): 11 | """Webhook trigger event type enumeration.""" 12 | 13 | MESSAGE = 1 14 | POSTBACK = 2 15 | FOLLOW = 3 16 | BEACON = 4 17 | TIME = 100 18 | MESSAGE_EDITOR = 101 19 | POSTBACK_EDITOR = 102 20 | 21 | 22 | class WebhookTriggerScheduleType(StrEnum): 23 | """Webhook trigger schedule type enumeration.""" 24 | 25 | DAILY = "daily" 26 | BUSINESS_HOUR = "business_hour" 27 | NON_BUSINESS_HOUR = "non_business_hour" 28 | MONTHLY = "monthly" 29 | DATE_RANGE = "date_range" 30 | 31 | 32 | class WebhookTriggerSchedule(BaseModel, ABC): 33 | """Abstract base class for webhook trigger schedule.""" 34 | 35 | @abstractmethod 36 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 37 | """Get the schedule type.""" 38 | pass 39 | 40 | @abstractmethod 41 | def get_schedule_settings(self) -> dict[str, object] | None: 42 | """Get the schedule settings.""" 43 | pass 44 | 45 | 46 | class DailySchedule(WebhookTriggerSchedule): 47 | """Daily trigger schedule.""" 48 | 49 | start_time: str 50 | end_time: str 51 | 52 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 53 | """Get the schedule type.""" 54 | return WebhookTriggerScheduleType.DAILY 55 | 56 | def get_schedule_settings(self) -> dict[str, object]: 57 | """Get the schedule settings.""" 58 | return {"start_time": self.start_time, "end_time": self.end_time} 59 | 60 | 61 | class MonthlySchedule(WebhookTriggerSchedule): 62 | """Monthly trigger schedule.""" 63 | 64 | day: int 65 | start_time: str 66 | end_time: str 67 | 68 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 69 | """Get the schedule type.""" 70 | return WebhookTriggerScheduleType.MONTHLY 71 | 72 | def get_schedule_settings(self) -> dict[str, object]: 73 | """Get the schedule settings.""" 74 | return {"day": self.day, "start_time": self.start_time, "end_time": self.end_time} 75 | 76 | 77 | class DateRangeSchedule(WebhookTriggerSchedule): 78 | """Date range trigger schedule.""" 79 | 80 | start_date: str 81 | end_date: str 82 | 83 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 84 | """Get the schedule type.""" 85 | return WebhookTriggerScheduleType.DATE_RANGE 86 | 87 | def get_schedule_settings(self) -> dict[str, object]: 88 | """Get the schedule settings.""" 89 | return {"start_date": self.start_date, "end_date": self.end_date} 90 | 91 | 92 | class BusinessHourSchedule(WebhookTriggerSchedule): 93 | """Business hour trigger schedule.""" 94 | 95 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 96 | """Get the schedule type.""" 97 | return WebhookTriggerScheduleType.BUSINESS_HOUR 98 | 99 | def get_schedule_settings(self) -> dict[str, object] | None: 100 | """Get the schedule settings.""" 101 | return None 102 | 103 | 104 | class NonBusinessHourSchedule(WebhookTriggerSchedule): 105 | """Non-business hour trigger schedule.""" 106 | 107 | def get_schedule_type(self) -> WebhookTriggerScheduleType: 108 | """Get the schedule type.""" 109 | return WebhookTriggerScheduleType.NON_BUSINESS_HOUR 110 | 111 | def get_schedule_settings(self) -> dict[str, object] | None: 112 | """Get the schedule settings.""" 113 | return None 114 | 115 | 116 | class WebhookTriggerScheduleSettings(BaseModel): 117 | """Webhook trigger schedule settings.""" 118 | 119 | schedules: list[ 120 | DailySchedule | MonthlySchedule | DateRangeSchedule | BusinessHourSchedule | NonBusinessHourSchedule 121 | ] 122 | 123 | class Config: 124 | """Pydantic configuration.""" 125 | 126 | use_enum_values = True 127 | 128 | 129 | class WebhookTriggerSetting(BaseModel): 130 | """Webhook trigger setting domain model. 131 | 132 | Represents the channel-level configuration for webhook triggers (Auto-Reply). 133 | """ 134 | 135 | id: int 136 | auto_reply_id: int 137 | bot_id: int 138 | enable: bool 139 | name: str # Will be deprecated 140 | event_type: WebhookTriggerEventType 141 | trigger_code: str | None = None # Will be deprecated 142 | trigger_schedule_type: WebhookTriggerScheduleType | None = None # Will be deprecated 143 | trigger_schedule_settings: dict[str, object] | None = None # Will be deprecated 144 | created_at: datetime 145 | updated_at: datetime 146 | archived: bool = False 147 | extra: dict[str, object] | None = None 148 | 149 | def is_active(self) -> bool: 150 | """Check if the webhook trigger setting is active.""" 151 | return self.enable and not self.archived 152 | 153 | class Config: 154 | """Pydantic configuration.""" 155 | 156 | use_enum_values = True 157 | -------------------------------------------------------------------------------- /legacy/line/utils/cache.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | 4 | from django.core.cache import cache 5 | from django.db.models import Prefetch 6 | from django.forms.models import model_to_dict 7 | 8 | from line.models import WebhookTriggerMessage, WebhookTriggerSetting 9 | from rubato.cache_keys import ( 10 | LINE_WEBHOOK_TRIGGER_INFO_V2, 11 | ) 12 | 13 | 14 | def refresh_webhook_trigger_info_v2(bot_id: int = 0, channel_id: str = ""): 15 | """ 16 | trigger_data_template = { 17 | "message": { 18 | "keyword_1": {}, 19 | "keyword_2": {} 20 | }, 21 | "follow": { 22 | }, 23 | "postback": { 24 | "category_action_param": {} 25 | }, 26 | "beacon": { 27 | "hw_id1": { 28 | "id": 1, 29 | "enable": True, 30 | "name": "beacon_1", 31 | "bot": 1, 32 | "event_type": 4, 33 | "trigger_code": "000002477b", 34 | "tag": ["beacon"], 35 | "messages": { 36 | 1: { 37 | "id": 1, 38 | "messages": [ 39 | { 40 | "data": { 41 | "text": "new friend xxxx" 42 | }, 43 | "module_id": 1, 44 | "parameters": [], 45 | } 46 | ], 47 | "enable": True, 48 | "setting": 1, 49 | "message_type": 1, 50 | }, 51 | 2: { 52 | "id": 2, 53 | "messages": [ 54 | { 55 | "data": { 56 | "text": "old friend xxxx" 57 | }, 58 | "module_id": 1, 59 | "parameters": [], 60 | } 61 | ], 62 | "enable": True, 63 | "setting": 1, 64 | "message_type": 2, 65 | }, 66 | }, 67 | }, 68 | "hw_id2": {} 69 | }, 70 | "time": { 71 | "HH:MM:SSHH:MM:SS": {} 72 | } 73 | } 74 | """ 75 | trigger_settings = WebhookTriggerSetting.objects.filter( 76 | bot_id=bot_id, enable=True, archived=False 77 | ).prefetch_related( 78 | Prefetch( 79 | "messages", 80 | queryset=WebhookTriggerMessage.objects.filter(enable=True), 81 | to_attr="reply_messages", 82 | ) 83 | ) 84 | trigger_setting_infos = defaultdict(dict) 85 | for trigger_setting in trigger_settings: 86 | event_type = trigger_setting.event_type 87 | trigger_code = trigger_setting.trigger_code 88 | trigger_setting_dict = model_to_dict(trigger_setting) 89 | messages = {} 90 | for reply_message in trigger_setting.reply_messages: 91 | reply_message_info = model_to_dict(reply_message) 92 | messages[reply_message_info["trigger_type"]] = reply_message_info 93 | trigger_setting_dict["messages"] = messages 94 | if event_type == WebhookTriggerSetting.BEACON: 95 | trigger_setting_infos["beacon"][trigger_code] = trigger_setting_dict 96 | elif event_type == WebhookTriggerSetting.FOLLOW: 97 | trigger_setting_infos["follow"] = trigger_setting_dict 98 | elif event_type == WebhookTriggerSetting.TIME: 99 | # Save to cache only if trigger_code is in the format %H:%M:%S%H:%M:%S 100 | # because the new time trigger structure will not be judged by trigger_code 101 | if trigger_code and re.fullmatch( 102 | r"\d{2}:\d{2}:\d{2}\d{2}:\d{2}:\d{2}", trigger_code 103 | ): 104 | trigger_setting_infos["time"][trigger_code] = trigger_setting_dict 105 | elif event_type in [ 106 | WebhookTriggerSetting.POSTBACK, 107 | WebhookTriggerSetting.POSTBACK_EDITOR, 108 | ]: 109 | trigger_setting_infos["postback"][trigger_code] = trigger_setting_dict 110 | elif event_type in [ 111 | WebhookTriggerSetting.MESSAGE, 112 | WebhookTriggerSetting.MESSAGE_EDITOR, 113 | ]: 114 | trigger_setting_infos["message"][trigger_code] = trigger_setting_dict 115 | 116 | key = LINE_WEBHOOK_TRIGGER_INFO_V2.format(channel_id=channel_id) 117 | cache.set(key, trigger_setting_infos, timeout=60 * 60 * 48) 118 | return trigger_setting_infos 119 | 120 | 121 | def get_webhook_trigger_info_v2(bot_id: int = 0, channel_id: str = ""): 122 | trigger_setting_info_key = LINE_WEBHOOK_TRIGGER_INFO_V2.format( 123 | channel_id=channel_id 124 | ) 125 | trigger_setting_infos = cache.get(trigger_setting_info_key) 126 | if not trigger_setting_infos: 127 | trigger_setting_infos = refresh_webhook_trigger_info_v2( 128 | bot_id=bot_id, channel_id=channel_id 129 | ) 130 | return trigger_setting_infos 131 | -------------------------------------------------------------------------------- /go_src/internal/domain/auto_reply/webhook_trigger.go: -------------------------------------------------------------------------------- 1 | package auto_reply 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // WebhookTriggerEventType represents the type of WebhookTriggerSetting 9 | type WebhookTriggerEventType int 10 | 11 | const ( 12 | EventTypeMessage WebhookTriggerEventType = 1 13 | EventTypePostback WebhookTriggerEventType = 2 14 | EventTypeFollow WebhookTriggerEventType = 3 15 | EventTypeBeacon WebhookTriggerEventType = 4 16 | EventTypeTime WebhookTriggerEventType = 100 17 | EventTypeMessageEditor WebhookTriggerEventType = 101 18 | EventTypePostbackEditor WebhookTriggerEventType = 102 19 | ) 20 | 21 | // WebhookTriggerSetting represents the channel-level configuration for webhook triggers (Auto-Reply). 22 | type WebhookTriggerSetting struct { 23 | ID int `json:"id"` 24 | AutoReplyID int `json:"auto_reply_id"` 25 | BotID int `json:"bot_id"` 26 | Enable bool `json:"enable"` 27 | Name string `json:"name"` // Will be deprecated 28 | EventType WebhookTriggerEventType `json:"event_type"` 29 | TriggerCode *string `json:"trigger_code,omitempty"` // Will be deprecated 30 | TriggerScheduleType *WebhookTriggerScheduleType `json:"trigger_schedule_type,omitempty"` // Will be deprecated 31 | TriggerScheduleSettings json.RawMessage `json:"trigger_schedule_settings,omitempty"` // Will be deprecated 32 | CreatedAt time.Time `json:"created_at"` 33 | UpdatedAt time.Time `json:"updated_at"` 34 | Archived bool `json:"archived"` 35 | Extra json.RawMessage `json:"extra,omitempty"` 36 | } 37 | 38 | func (wts *WebhookTriggerSetting) IsActive() bool { 39 | return wts.Enable && !wts.Archived 40 | } 41 | 42 | // WebhookTriggerScheduleType represents the type of trigger schedule for webhook trigger. 43 | type WebhookTriggerScheduleType string 44 | 45 | const ( 46 | WebhookTriggerScheduleTypeDaily WebhookTriggerScheduleType = "daily" 47 | WebhookTriggerScheduleTypeBusinessHour WebhookTriggerScheduleType = "business_hour" 48 | WebhookTriggerScheduleTypeNonBusinessHour WebhookTriggerScheduleType = "non_business_hour" 49 | WebhookTriggerScheduleTypeMonthly WebhookTriggerScheduleType = "monthly" 50 | WebhookTriggerScheduleTypeDateRange WebhookTriggerScheduleType = "date_range" 51 | ) 52 | 53 | // WebhookTriggerScheduleSettings represents the settings for the trigger schedule. 54 | type WebhookTriggerScheduleSettings struct { 55 | Schedules []WebhookTriggerSchedule `json:"schedules"` 56 | } 57 | 58 | type WebhookTriggerSchedule interface { 59 | GetScheduleType() WebhookTriggerScheduleType 60 | GetScheduleSettings() json.RawMessage 61 | } 62 | 63 | // DailySchedule represents a daily trigger schedule. 64 | type DailySchedule struct { 65 | StartTime string `json:"start_time"` 66 | EndTime string `json:"end_time"` 67 | } 68 | 69 | func (d *DailySchedule) GetScheduleType() WebhookTriggerScheduleType { 70 | return WebhookTriggerScheduleTypeDaily 71 | } 72 | 73 | func (d *DailySchedule) GetScheduleSettings() json.RawMessage { 74 | settings, err := json.Marshal(d) 75 | if err != nil { 76 | return nil 77 | } 78 | return settings 79 | } 80 | 81 | // MonthlySchedule represents a monthly trigger schedule. 82 | type MonthlySchedule struct { 83 | Day int `json:"day"` 84 | StartTime string `json:"start_time"` 85 | EndTime string `json:"end_time"` 86 | } 87 | 88 | func (m *MonthlySchedule) GetScheduleType() WebhookTriggerScheduleType { 89 | return WebhookTriggerScheduleTypeMonthly 90 | } 91 | 92 | func (m *MonthlySchedule) GetScheduleSettings() json.RawMessage { 93 | settings, err := json.Marshal(m) 94 | if err != nil { 95 | return nil 96 | } 97 | return settings 98 | } 99 | 100 | // DateRangeSchedule represents a date range trigger schedule. 101 | type DateRangeSchedule struct { 102 | StartDate string `json:"start_date"` 103 | EndDate string `json:"end_date"` 104 | } 105 | 106 | func (d *DateRangeSchedule) GetScheduleType() WebhookTriggerScheduleType { 107 | return WebhookTriggerScheduleTypeDateRange 108 | } 109 | 110 | func (d *DateRangeSchedule) GetScheduleSettings() json.RawMessage { 111 | settings, err := json.Marshal(d) 112 | if err != nil { 113 | return nil 114 | } 115 | return settings 116 | } 117 | 118 | // BusinessHourSchedule represents a business hour trigger schedule. 119 | type BusinessHourSchedule struct{} 120 | 121 | func (b *BusinessHourSchedule) GetScheduleType() WebhookTriggerScheduleType { 122 | return WebhookTriggerScheduleTypeBusinessHour 123 | } 124 | 125 | func (b *BusinessHourSchedule) GetScheduleSettings() json.RawMessage { 126 | return nil 127 | } 128 | 129 | // NonBusinessHourSchedule represents a non-business hour trigger schedule. 130 | type NonBusinessHourSchedule struct{} 131 | 132 | func (n *NonBusinessHourSchedule) GetScheduleType() WebhookTriggerScheduleType { 133 | return WebhookTriggerScheduleTypeNonBusinessHour 134 | } 135 | 136 | func (n *NonBusinessHourSchedule) GetScheduleSettings() json.RawMessage { 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /cheat_sheet/go/1_rewrite_brownfield/internal/domain/auto_reply/webhook_trigger.go: -------------------------------------------------------------------------------- 1 | package auto_reply 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // WebhookTriggerEventType represents the type of WebhookTriggerSetting 9 | type WebhookTriggerEventType int 10 | 11 | const ( 12 | EventTypeMessage WebhookTriggerEventType = 1 13 | EventTypePostback WebhookTriggerEventType = 2 14 | EventTypeFollow WebhookTriggerEventType = 3 15 | EventTypeBeacon WebhookTriggerEventType = 4 16 | EventTypeTime WebhookTriggerEventType = 100 17 | EventTypeMessageEditor WebhookTriggerEventType = 101 18 | EventTypePostbackEditor WebhookTriggerEventType = 102 19 | ) 20 | 21 | // WebhookTriggerSetting represents the channel-level configuration for webhook triggers (Auto-Reply). 22 | type WebhookTriggerSetting struct { 23 | ID int `json:"id"` 24 | AutoReplyID int `json:"auto_reply_id"` 25 | BotID int `json:"bot_id"` 26 | Enable bool `json:"enable"` 27 | Name string `json:"name"` // Will be deprecated 28 | EventType WebhookTriggerEventType `json:"event_type"` 29 | TriggerCode *string `json:"trigger_code,omitempty"` // Will be deprecated 30 | TriggerScheduleType *WebhookTriggerScheduleType `json:"trigger_schedule_type,omitempty"` // Will be deprecated 31 | TriggerScheduleSettings json.RawMessage `json:"trigger_schedule_settings,omitempty"` // Will be deprecated 32 | CreatedAt time.Time `json:"created_at"` 33 | UpdatedAt time.Time `json:"updated_at"` 34 | Archived bool `json:"archived"` 35 | Extra json.RawMessage `json:"extra,omitempty"` 36 | } 37 | 38 | func (wts *WebhookTriggerSetting) IsActive() bool { 39 | return wts.Enable && !wts.Archived 40 | } 41 | 42 | // WebhookTriggerScheduleType represents the type of trigger schedule for webhook trigger. 43 | type WebhookTriggerScheduleType string 44 | 45 | const ( 46 | WebhookTriggerScheduleTypeDaily WebhookTriggerScheduleType = "daily" 47 | WebhookTriggerScheduleTypeBusinessHour WebhookTriggerScheduleType = "business_hour" 48 | WebhookTriggerScheduleTypeNonBusinessHour WebhookTriggerScheduleType = "non_business_hour" 49 | WebhookTriggerScheduleTypeMonthly WebhookTriggerScheduleType = "monthly" 50 | WebhookTriggerScheduleTypeDateRange WebhookTriggerScheduleType = "date_range" 51 | ) 52 | 53 | // WebhookTriggerScheduleSettings represents the settings for the trigger schedule. 54 | type WebhookTriggerScheduleSettings struct { 55 | Schedules []WebhookTriggerSchedule `json:"schedules"` 56 | } 57 | 58 | type WebhookTriggerSchedule interface { 59 | GetScheduleType() WebhookTriggerScheduleType 60 | GetScheduleSettings() json.RawMessage 61 | } 62 | 63 | // DailySchedule represents a daily trigger schedule. 64 | type DailySchedule struct { 65 | StartTime string `json:"start_time"` 66 | EndTime string `json:"end_time"` 67 | } 68 | 69 | func (d *DailySchedule) GetScheduleType() WebhookTriggerScheduleType { 70 | return WebhookTriggerScheduleTypeDaily 71 | } 72 | 73 | func (d *DailySchedule) GetScheduleSettings() json.RawMessage { 74 | settings, err := json.Marshal(d) 75 | if err != nil { 76 | return nil 77 | } 78 | return settings 79 | } 80 | 81 | // MonthlySchedule represents a monthly trigger schedule. 82 | type MonthlySchedule struct { 83 | Day int `json:"day"` 84 | StartTime string `json:"start_time"` 85 | EndTime string `json:"end_time"` 86 | } 87 | 88 | func (m *MonthlySchedule) GetScheduleType() WebhookTriggerScheduleType { 89 | return WebhookTriggerScheduleTypeMonthly 90 | } 91 | 92 | func (m *MonthlySchedule) GetScheduleSettings() json.RawMessage { 93 | settings, err := json.Marshal(m) 94 | if err != nil { 95 | return nil 96 | } 97 | return settings 98 | } 99 | 100 | // DateRangeSchedule represents a date range trigger schedule. 101 | type DateRangeSchedule struct { 102 | StartDate string `json:"start_date"` 103 | EndDate string `json:"end_date"` 104 | } 105 | 106 | func (d *DateRangeSchedule) GetScheduleType() WebhookTriggerScheduleType { 107 | return WebhookTriggerScheduleTypeDateRange 108 | } 109 | 110 | func (d *DateRangeSchedule) GetScheduleSettings() json.RawMessage { 111 | settings, err := json.Marshal(d) 112 | if err != nil { 113 | return nil 114 | } 115 | return settings 116 | } 117 | 118 | // BusinessHourSchedule represents a business hour trigger schedule. 119 | type BusinessHourSchedule struct{} 120 | 121 | func (b *BusinessHourSchedule) GetScheduleType() WebhookTriggerScheduleType { 122 | return WebhookTriggerScheduleTypeBusinessHour 123 | } 124 | 125 | func (b *BusinessHourSchedule) GetScheduleSettings() json.RawMessage { 126 | return nil 127 | } 128 | 129 | // NonBusinessHourSchedule represents a non-business hour trigger schedule. 130 | type NonBusinessHourSchedule struct{} 131 | 132 | func (n *NonBusinessHourSchedule) GetScheduleType() WebhookTriggerScheduleType { 133 | return WebhookTriggerScheduleTypeNonBusinessHour 134 | } 135 | 136 | func (n *NonBusinessHourSchedule) GetScheduleSettings() json.RawMessage { 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /cheat_sheet/go/2_extend_feature/internal/domain/auto_reply/webhook_trigger.go: -------------------------------------------------------------------------------- 1 | package auto_reply 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // WebhookTriggerEventType represents the type of WebhookTriggerSetting 9 | type WebhookTriggerEventType int 10 | 11 | const ( 12 | EventTypeMessage WebhookTriggerEventType = 1 13 | EventTypePostback WebhookTriggerEventType = 2 14 | EventTypeFollow WebhookTriggerEventType = 3 15 | EventTypeBeacon WebhookTriggerEventType = 4 16 | EventTypeTime WebhookTriggerEventType = 100 17 | EventTypeMessageEditor WebhookTriggerEventType = 101 18 | EventTypePostbackEditor WebhookTriggerEventType = 102 19 | ) 20 | 21 | // WebhookTriggerSetting represents the channel-level configuration for webhook triggers (Auto-Reply). 22 | type WebhookTriggerSetting struct { 23 | ID int `json:"id"` 24 | AutoReplyID int `json:"auto_reply_id"` 25 | BotID int `json:"bot_id"` 26 | Enable bool `json:"enable"` 27 | Name string `json:"name"` // Will be deprecated 28 | EventType WebhookTriggerEventType `json:"event_type"` 29 | TriggerCode *string `json:"trigger_code,omitempty"` // Will be deprecated 30 | TriggerScheduleType *WebhookTriggerScheduleType `json:"trigger_schedule_type,omitempty"` // Will be deprecated 31 | TriggerScheduleSettings json.RawMessage `json:"trigger_schedule_settings,omitempty"` // Will be deprecated 32 | IGStoryIDs []string `json:"ig_story_ids,omitempty"` // IG Story IDs for story-specific triggers 33 | CreatedAt time.Time `json:"created_at"` 34 | UpdatedAt time.Time `json:"updated_at"` 35 | Archived bool `json:"archived"` 36 | Extra json.RawMessage `json:"extra,omitempty"` 37 | } 38 | 39 | func (wts *WebhookTriggerSetting) IsActive() bool { 40 | return wts.Enable && !wts.Archived 41 | } 42 | 43 | // WebhookTriggerScheduleType represents the type of trigger schedule for webhook trigger. 44 | type WebhookTriggerScheduleType string 45 | 46 | const ( 47 | WebhookTriggerScheduleTypeDaily WebhookTriggerScheduleType = "daily" 48 | WebhookTriggerScheduleTypeBusinessHour WebhookTriggerScheduleType = "business_hour" 49 | WebhookTriggerScheduleTypeNonBusinessHour WebhookTriggerScheduleType = "non_business_hour" 50 | WebhookTriggerScheduleTypeMonthly WebhookTriggerScheduleType = "monthly" 51 | WebhookTriggerScheduleTypeDateRange WebhookTriggerScheduleType = "date_range" 52 | ) 53 | 54 | // WebhookTriggerScheduleSettings represents the settings for the trigger schedule. 55 | type WebhookTriggerScheduleSettings struct { 56 | Schedules []WebhookTriggerSchedule `json:"schedules"` 57 | } 58 | 59 | type WebhookTriggerSchedule interface { 60 | GetScheduleType() WebhookTriggerScheduleType 61 | GetScheduleSettings() json.RawMessage 62 | } 63 | 64 | // DailySchedule represents a daily trigger schedule. 65 | type DailySchedule struct { 66 | StartTime string `json:"start_time"` 67 | EndTime string `json:"end_time"` 68 | } 69 | 70 | func (d *DailySchedule) GetScheduleType() WebhookTriggerScheduleType { 71 | return WebhookTriggerScheduleTypeDaily 72 | } 73 | 74 | func (d *DailySchedule) GetScheduleSettings() json.RawMessage { 75 | settings, err := json.Marshal(d) 76 | if err != nil { 77 | return nil 78 | } 79 | return settings 80 | } 81 | 82 | // MonthlySchedule represents a monthly trigger schedule. 83 | type MonthlySchedule struct { 84 | Day int `json:"day"` 85 | StartTime string `json:"start_time"` 86 | EndTime string `json:"end_time"` 87 | } 88 | 89 | func (m *MonthlySchedule) GetScheduleType() WebhookTriggerScheduleType { 90 | return WebhookTriggerScheduleTypeMonthly 91 | } 92 | 93 | func (m *MonthlySchedule) GetScheduleSettings() json.RawMessage { 94 | settings, err := json.Marshal(m) 95 | if err != nil { 96 | return nil 97 | } 98 | return settings 99 | } 100 | 101 | // DateRangeSchedule represents a date range trigger schedule. 102 | type DateRangeSchedule struct { 103 | StartDate string `json:"start_date"` 104 | EndDate string `json:"end_date"` 105 | } 106 | 107 | func (d *DateRangeSchedule) GetScheduleType() WebhookTriggerScheduleType { 108 | return WebhookTriggerScheduleTypeDateRange 109 | } 110 | 111 | func (d *DateRangeSchedule) GetScheduleSettings() json.RawMessage { 112 | settings, err := json.Marshal(d) 113 | if err != nil { 114 | return nil 115 | } 116 | return settings 117 | } 118 | 119 | // BusinessHourSchedule represents a business hour trigger schedule. 120 | type BusinessHourSchedule struct{} 121 | 122 | func (b *BusinessHourSchedule) GetScheduleType() WebhookTriggerScheduleType { 123 | return WebhookTriggerScheduleTypeBusinessHour 124 | } 125 | 126 | func (b *BusinessHourSchedule) GetScheduleSettings() json.RawMessage { 127 | return nil 128 | } 129 | 130 | // NonBusinessHourSchedule represents a non-business hour trigger schedule. 131 | type NonBusinessHourSchedule struct{} 132 | 133 | func (n *NonBusinessHourSchedule) GetScheduleType() WebhookTriggerScheduleType { 134 | return WebhookTriggerScheduleTypeNonBusinessHour 135 | } 136 | 137 | func (n *NonBusinessHourSchedule) GetScheduleSettings() json.RawMessage { 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /go_src/internal/domain/common/error_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDomainError_Option(t *testing.T) { 12 | t.Parallel() 13 | 14 | msg := "random client message" 15 | status := http.StatusBadRequest 16 | detail := map[string]interface{}{ 17 | "channel_id": 123, 18 | "member_name": "who am I?", 19 | "tag_id": []int{1, 2, 3, 4}, 20 | } 21 | 22 | // Test cases 23 | testCases := []struct { 24 | Name string 25 | TestError Error 26 | WithMsg bool 27 | WitStatus bool 28 | WitDeniedPermission bool 29 | WithDetail bool 30 | }{ 31 | { 32 | Name: "with client msg", 33 | TestError: NewError(ErrorCodeInternalProcess, nil, WithMsg(msg)), 34 | WithMsg: true, 35 | }, 36 | { 37 | Name: "with proxy HTTP status", 38 | TestError: NewError(ErrorCodeRemoteClientError, nil, WithStatus(status)), 39 | WitStatus: true, 40 | }, 41 | { 42 | Name: "with detail", 43 | TestError: NewError(ErrorCodeInternalProcess, nil, WithDetail(detail)), 44 | WithDetail: true, 45 | }, 46 | } 47 | 48 | for i := range testCases { 49 | c := testCases[i] 50 | t.Run(c.Name, func(t *testing.T) { 51 | err := c.TestError 52 | 53 | var domainError DomainError 54 | if errors.As(err, &domainError) { 55 | if c.WithMsg { 56 | assert.EqualValues(t, msg, domainError.ClientMsg()) 57 | } 58 | if c.WitStatus { 59 | assert.EqualValues(t, status, domainError.RemoteHTTPStatus()) 60 | } 61 | if c.WitDeniedPermission { 62 | assert.Contains(t, domainError.Error(), "no permission to") 63 | } 64 | if c.WithDetail { 65 | assert.Contains(t, domainError.Detail(), "channel_id") 66 | assert.Contains(t, domainError.Detail(), "member_name") 67 | assert.Equal(t, detail, domainError.Detail()) 68 | } 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestDomainError_ErrorMapping(t *testing.T) { 75 | t.Parallel() 76 | 77 | // Test cases 78 | testCases := []struct { 79 | Name string 80 | TestError Error 81 | ExpectErrorName string 82 | ExpectCategory string 83 | ExpectHTTPStatus int 84 | ExpectRemoteHTTPStatus int 85 | }{ 86 | { 87 | Name: "internal process", 88 | TestError: NewError(ErrorCodeInternalProcess, nil), 89 | ExpectErrorName: ErrorCodeInternalProcess.Name, 90 | ExpectHTTPStatus: http.StatusInternalServerError, 91 | }, 92 | { 93 | Name: "permission denied", 94 | TestError: NewError(ErrorCodeAuthPermissionDenied, nil), 95 | ExpectErrorName: ErrorCodeAuthPermissionDenied.Name, 96 | ExpectHTTPStatus: http.StatusForbidden, 97 | }, 98 | { 99 | Name: "not authenticated", 100 | TestError: NewError(ErrorCodeAuthNotAuthenticated, nil), 101 | ExpectErrorName: ErrorCodeAuthNotAuthenticated.Name, 102 | ExpectHTTPStatus: http.StatusUnauthorized, 103 | }, 104 | { 105 | Name: "invalid parameter", 106 | TestError: NewError(ErrorCodeParameterInvalid, nil), 107 | ExpectErrorName: ErrorCodeParameterInvalid.Name, 108 | ExpectHTTPStatus: http.StatusBadRequest, 109 | }, { 110 | Name: "message content invalid", 111 | TestError: NewError(ErrorCodeMessageContentInvalid, nil), 112 | ExpectErrorName: ErrorCodeMessageContentInvalid.Name, 113 | ExpectHTTPStatus: http.StatusBadRequest, 114 | }, { 115 | Name: "unsupported channel type", 116 | TestError: NewError(ErrorCodeUnsupportedChannelType, nil), 117 | ExpectErrorName: ErrorCodeUnsupportedChannelType.Name, 118 | ExpectHTTPStatus: http.StatusBadRequest, 119 | }, 120 | { 121 | Name: "resource not found", 122 | TestError: NewError(ErrorCodeResourceNotFound, nil), 123 | ExpectErrorName: ErrorCodeResourceNotFound.Name, 124 | ExpectHTTPStatus: http.StatusNotFound, 125 | }, 126 | { 127 | Name: "remote client error", 128 | TestError: NewError(ErrorCodeRemoteClientError, nil, WithStatus(http.StatusUnauthorized)), 129 | ExpectErrorName: ErrorCodeRemoteClientError.Name, 130 | ExpectHTTPStatus: http.StatusBadRequest, 131 | ExpectRemoteHTTPStatus: http.StatusUnauthorized, 132 | }, { 133 | Name: "remote server error", 134 | TestError: NewError(ErrorCodeRemoteServerError, nil, WithStatus(http.StatusInternalServerError)), 135 | ExpectErrorName: ErrorCodeRemoteServerError.Name, 136 | ExpectHTTPStatus: http.StatusBadGateway, 137 | ExpectRemoteHTTPStatus: http.StatusInternalServerError, 138 | }, 139 | 140 | { 141 | Name: "unknown error", 142 | TestError: NewError(ErrorCode{}, nil), 143 | ExpectErrorName: "UNKNOWN_ERROR", 144 | ExpectCategory: "UNKNOWN_ERROR", 145 | ExpectHTTPStatus: http.StatusInternalServerError, 146 | }, 147 | } 148 | 149 | for i := range testCases { 150 | c := testCases[i] 151 | t.Run(c.Name, func(t *testing.T) { 152 | err := c.TestError 153 | 154 | var domainError DomainError 155 | if errors.As(err, &domainError) { 156 | assert.Equal(t, c.ExpectErrorName, domainError.Name()) 157 | assert.Equal(t, c.ExpectHTTPStatus, domainError.HTTPStatus()) 158 | assert.Equal(t, c.ExpectRemoteHTTPStatus, domainError.RemoteHTTPStatus()) 159 | } else { 160 | t.Error("failed to match error type") 161 | } 162 | }) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /python_src/tests/domain/common/test_error_code.py: -------------------------------------------------------------------------------- 1 | """Tests for error code functionality.""" 2 | 3 | from http import HTTPStatus 4 | 5 | import pytest 6 | from pydantic import ValidationError 7 | 8 | from internal.domain.common.error_code import ( 9 | FORBIDDEN, 10 | INTERNAL_ERROR, 11 | NOT_FOUND, 12 | UNAUTHORIZED, 13 | UNKNOWN_ERROR, 14 | VALIDATION_ERROR, 15 | ErrorCode, 16 | ) 17 | 18 | 19 | class TestErrorCode: 20 | """Test ErrorCode pydantic model.""" 21 | 22 | def test_create_error_code_with_explicit_status(self): 23 | """Test creating ErrorCode with explicit status code.""" 24 | error = ErrorCode(name="TEST_ERROR", status_code=HTTPStatus.BAD_REQUEST) 25 | 26 | assert error.name == "TEST_ERROR" 27 | assert error.status_code == HTTPStatus.BAD_REQUEST 28 | assert error.status_code == 400 29 | 30 | def test_create_error_code_with_default_status(self): 31 | """Test creating ErrorCode with default status code.""" 32 | error = ErrorCode(name="DEFAULT_ERROR") 33 | 34 | assert error.name == "DEFAULT_ERROR" 35 | assert error.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 36 | assert error.status_code == 500 37 | 38 | def test_error_code_immutability(self): 39 | """Test that ErrorCode is immutable (frozen).""" 40 | error = ErrorCode(name="IMMUTABLE_ERROR", status_code=HTTPStatus.BAD_REQUEST) 41 | 42 | with pytest.raises(ValidationError): 43 | error.name = "CHANGED_NAME" 44 | 45 | with pytest.raises(ValidationError): 46 | error.status_code = HTTPStatus.NOT_FOUND 47 | 48 | def test_error_code_equality(self): 49 | """Test ErrorCode equality comparison.""" 50 | error1 = ErrorCode(name="TEST_ERROR", status_code=HTTPStatus.BAD_REQUEST) 51 | error2 = ErrorCode(name="TEST_ERROR", status_code=HTTPStatus.BAD_REQUEST) 52 | error3 = ErrorCode(name="DIFFERENT_ERROR", status_code=HTTPStatus.BAD_REQUEST) 53 | 54 | assert error1 == error2 55 | assert error1 != error3 56 | 57 | def test_error_code_string_representation(self): 58 | """Test ErrorCode string representation.""" 59 | error = ErrorCode(name="TEST_ERROR", status_code=HTTPStatus.BAD_REQUEST) 60 | 61 | str_repr = str(error) 62 | assert "TEST_ERROR" in str_repr 63 | assert "400" in str_repr 64 | 65 | def test_error_code_validation(self): 66 | """Test ErrorCode validation.""" 67 | # Valid name 68 | ErrorCode(name="VALID_NAME", status_code=HTTPStatus.OK) 69 | 70 | # Empty name should work 71 | ErrorCode(name="", status_code=HTTPStatus.OK) 72 | 73 | # Invalid status code type should raise ValidationError 74 | with pytest.raises(ValidationError): 75 | ErrorCode(name="TEST_ERROR", status_code="invalid") # type: ignore[arg-type] 76 | 77 | def test_error_code_serialization(self): 78 | """Test ErrorCode serialization to dict.""" 79 | error = ErrorCode(name="SERIALIZE_ERROR", status_code=HTTPStatus.NOT_FOUND) 80 | 81 | data = error.model_dump() 82 | assert data == {"name": "SERIALIZE_ERROR", "status_code": 404} 83 | 84 | def test_error_code_deserialization(self): 85 | """Test ErrorCode deserialization from dict.""" 86 | data = {"name": "DESERIALIZE_ERROR", "status_code": 403} 87 | 88 | error = ErrorCode.model_validate(data) 89 | assert error.name == "DESERIALIZE_ERROR" 90 | assert error.status_code == HTTPStatus.FORBIDDEN 91 | 92 | 93 | class TestPredefinedErrorCodes: 94 | """Test predefined error code constants.""" 95 | 96 | def test_unknown_error(self): 97 | """Test UNKNOWN_ERROR constant.""" 98 | assert UNKNOWN_ERROR.name == "UNKNOWN_ERROR" 99 | assert UNKNOWN_ERROR.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 100 | 101 | def test_validation_error(self): 102 | """Test VALIDATION_ERROR constant.""" 103 | assert VALIDATION_ERROR.name == "VALIDATION_ERROR" 104 | assert VALIDATION_ERROR.status_code == HTTPStatus.BAD_REQUEST 105 | 106 | def test_not_found_error(self): 107 | """Test NOT_FOUND constant.""" 108 | assert NOT_FOUND.name == "NOT_FOUND" 109 | assert NOT_FOUND.status_code == HTTPStatus.NOT_FOUND 110 | 111 | def test_unauthorized_error(self): 112 | """Test UNAUTHORIZED constant.""" 113 | assert UNAUTHORIZED.name == "UNAUTHORIZED" 114 | assert UNAUTHORIZED.status_code == HTTPStatus.UNAUTHORIZED 115 | 116 | def test_forbidden_error(self): 117 | """Test FORBIDDEN constant.""" 118 | assert FORBIDDEN.name == "FORBIDDEN" 119 | assert FORBIDDEN.status_code == HTTPStatus.FORBIDDEN 120 | 121 | def test_internal_error(self): 122 | """Test INTERNAL_ERROR constant.""" 123 | assert INTERNAL_ERROR.name == "INTERNAL_ERROR" 124 | assert INTERNAL_ERROR.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 125 | 126 | def test_predefined_errors_are_immutable(self): 127 | """Test that predefined error codes are immutable.""" 128 | with pytest.raises(ValidationError): 129 | VALIDATION_ERROR.name = "CHANGED_NAME" 130 | 131 | with pytest.raises(ValidationError): 132 | NOT_FOUND.status_code = HTTPStatus.OK 133 | 134 | def test_all_predefined_errors_exist(self): 135 | """Test that all expected predefined errors exist and have correct types.""" 136 | predefined_errors = [UNKNOWN_ERROR, VALIDATION_ERROR, NOT_FOUND, UNAUTHORIZED, FORBIDDEN, INTERNAL_ERROR] 137 | 138 | for error in predefined_errors: 139 | assert isinstance(error, ErrorCode) 140 | assert isinstance(error.name, str) 141 | assert isinstance(error.status_code, int) 142 | assert error.name # Not empty 143 | assert 100 <= error.status_code <= 599 # Valid HTTP status code range 144 | -------------------------------------------------------------------------------- /cheat_sheet/python/2_extend_feature/tests/domain/common/test_error_code.py: -------------------------------------------------------------------------------- 1 | """Tests for error code functionality.""" 2 | 3 | from http import HTTPStatus 4 | 5 | import pytest 6 | from pydantic import ValidationError 7 | 8 | from internal.domain.common.error_code import ( 9 | FORBIDDEN, 10 | INTERNAL_ERROR, 11 | NOT_FOUND, 12 | UNAUTHORIZED, 13 | UNKNOWN_ERROR, 14 | VALIDATION_ERROR, 15 | ErrorCode, 16 | ) 17 | 18 | 19 | class TestErrorCode: 20 | """Test ErrorCode pydantic model.""" 21 | 22 | def test_create_error_code_with_explicit_status(self): 23 | """Test creating ErrorCode with explicit status code.""" 24 | error = ErrorCode(name="TEST_ERROR", status_code=HTTPStatus.BAD_REQUEST) 25 | 26 | assert error.name == "TEST_ERROR" 27 | assert error.status_code == HTTPStatus.BAD_REQUEST 28 | assert error.status_code == 400 29 | 30 | def test_create_error_code_with_default_status(self): 31 | """Test creating ErrorCode with default status code.""" 32 | error = ErrorCode(name="DEFAULT_ERROR") 33 | 34 | assert error.name == "DEFAULT_ERROR" 35 | assert error.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 36 | assert error.status_code == 500 37 | 38 | def test_error_code_immutability(self): 39 | """Test that ErrorCode is immutable (frozen).""" 40 | error = ErrorCode(name="IMMUTABLE_ERROR", status_code=HTTPStatus.BAD_REQUEST) 41 | 42 | with pytest.raises(ValidationError): 43 | error.name = "CHANGED_NAME" 44 | 45 | with pytest.raises(ValidationError): 46 | error.status_code = HTTPStatus.NOT_FOUND 47 | 48 | def test_error_code_equality(self): 49 | """Test ErrorCode equality comparison.""" 50 | error1 = ErrorCode(name="TEST_ERROR", status_code=HTTPStatus.BAD_REQUEST) 51 | error2 = ErrorCode(name="TEST_ERROR", status_code=HTTPStatus.BAD_REQUEST) 52 | error3 = ErrorCode(name="DIFFERENT_ERROR", status_code=HTTPStatus.BAD_REQUEST) 53 | 54 | assert error1 == error2 55 | assert error1 != error3 56 | 57 | def test_error_code_string_representation(self): 58 | """Test ErrorCode string representation.""" 59 | error = ErrorCode(name="TEST_ERROR", status_code=HTTPStatus.BAD_REQUEST) 60 | 61 | str_repr = str(error) 62 | assert "TEST_ERROR" in str_repr 63 | assert "400" in str_repr 64 | 65 | def test_error_code_validation(self): 66 | """Test ErrorCode validation.""" 67 | # Valid name 68 | ErrorCode(name="VALID_NAME", status_code=HTTPStatus.OK) 69 | 70 | # Empty name should work 71 | ErrorCode(name="", status_code=HTTPStatus.OK) 72 | 73 | # Invalid status code type should raise ValidationError 74 | with pytest.raises(ValidationError): 75 | ErrorCode(name="TEST_ERROR", status_code="invalid") # type: ignore[arg-type] 76 | 77 | def test_error_code_serialization(self): 78 | """Test ErrorCode serialization to dict.""" 79 | error = ErrorCode(name="SERIALIZE_ERROR", status_code=HTTPStatus.NOT_FOUND) 80 | 81 | data = error.model_dump() 82 | assert data == {"name": "SERIALIZE_ERROR", "status_code": 404} 83 | 84 | def test_error_code_deserialization(self): 85 | """Test ErrorCode deserialization from dict.""" 86 | data = {"name": "DESERIALIZE_ERROR", "status_code": 403} 87 | 88 | error = ErrorCode.model_validate(data) 89 | assert error.name == "DESERIALIZE_ERROR" 90 | assert error.status_code == HTTPStatus.FORBIDDEN 91 | 92 | 93 | class TestPredefinedErrorCodes: 94 | """Test predefined error code constants.""" 95 | 96 | def test_unknown_error(self): 97 | """Test UNKNOWN_ERROR constant.""" 98 | assert UNKNOWN_ERROR.name == "UNKNOWN_ERROR" 99 | assert UNKNOWN_ERROR.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 100 | 101 | def test_validation_error(self): 102 | """Test VALIDATION_ERROR constant.""" 103 | assert VALIDATION_ERROR.name == "VALIDATION_ERROR" 104 | assert VALIDATION_ERROR.status_code == HTTPStatus.BAD_REQUEST 105 | 106 | def test_not_found_error(self): 107 | """Test NOT_FOUND constant.""" 108 | assert NOT_FOUND.name == "NOT_FOUND" 109 | assert NOT_FOUND.status_code == HTTPStatus.NOT_FOUND 110 | 111 | def test_unauthorized_error(self): 112 | """Test UNAUTHORIZED constant.""" 113 | assert UNAUTHORIZED.name == "UNAUTHORIZED" 114 | assert UNAUTHORIZED.status_code == HTTPStatus.UNAUTHORIZED 115 | 116 | def test_forbidden_error(self): 117 | """Test FORBIDDEN constant.""" 118 | assert FORBIDDEN.name == "FORBIDDEN" 119 | assert FORBIDDEN.status_code == HTTPStatus.FORBIDDEN 120 | 121 | def test_internal_error(self): 122 | """Test INTERNAL_ERROR constant.""" 123 | assert INTERNAL_ERROR.name == "INTERNAL_ERROR" 124 | assert INTERNAL_ERROR.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 125 | 126 | def test_predefined_errors_are_immutable(self): 127 | """Test that predefined error codes are immutable.""" 128 | with pytest.raises(ValidationError): 129 | VALIDATION_ERROR.name = "CHANGED_NAME" 130 | 131 | with pytest.raises(ValidationError): 132 | NOT_FOUND.status_code = HTTPStatus.OK 133 | 134 | def test_all_predefined_errors_exist(self): 135 | """Test that all expected predefined errors exist and have correct types.""" 136 | predefined_errors = [UNKNOWN_ERROR, VALIDATION_ERROR, NOT_FOUND, UNAUTHORIZED, FORBIDDEN, INTERNAL_ERROR] 137 | 138 | for error in predefined_errors: 139 | assert isinstance(error, ErrorCode) 140 | assert isinstance(error.name, str) 141 | assert isinstance(error.status_code, int) 142 | assert error.name # Not empty 143 | assert 100 <= error.status_code <= 599 # Valid HTTP status code range 144 | --------------------------------------------------------------------------------