├── tests ├── __init__.py ├── test_openapi.py ├── test_factory.py ├── test_rcs_pydantic.py └── factory.py ├── .flake8 ├── mkdocs.yml ├── scripts ├── publish.sh └── lint-test.sh ├── docs └── index.md ├── .pre-commit-config.yaml ├── rcs_pydantic ├── exceptions.py ├── __init__.py ├── enums.py ├── main.py ├── scheme.py └── errors.py ├── LICENSE ├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── pyproject.toml ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=120 3 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: My Docs 2 | 3 | 4 | theme: 5 | name: 'material' 6 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | poetry publish --build 6 | -------------------------------------------------------------------------------- /scripts/lint-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | poetry run black rcs_pydantic --check 6 | poetry run isort --check-only rcs_pydantic 7 | poetry run ruff check --exit-zero . 8 | -------------------------------------------------------------------------------- /tests/test_openapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from rcs_pydantic import SendInfo 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.post("/") 9 | def read_root(request, payload: SendInfo): 10 | return {"Hello": "World"} 11 | 12 | 13 | def test_openapi(): 14 | assert app.openapi() 15 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to MkDocs 2 | 3 | For full documentation visit [mkdocs.org](https://www.mkdocs.org). 4 | 5 | ## Commands 6 | 7 | * `mkdocs new [dir-name]` - Create a new project. 8 | * `mkdocs serve` - Start the live-reloading docs server. 9 | * `mkdocs build` - Build the documentation site. 10 | * `mkdocs -h` - Print help message and exit. 11 | 12 | ## Project layout 13 | 14 | mkdocs.yml # The configuration file. 15 | docs/ 16 | index.md # The documentation homepage. 17 | ... # Other markdown pages, images and other files. 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/charliermarsh/ruff-pre-commit 3 | # Ruff version. 4 | rev: 'v0.0.254' 5 | hooks: 6 | - id: ruff 7 | args: [--fix, --exit-non-zero-on-fix] 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v4.4.0 10 | hooks: 11 | - id: check-yaml 12 | args: [--allow-multiple-documents] 13 | - id: end-of-file-fixer 14 | - id: trailing-whitespace 15 | - repo: https://github.com/psf/black 16 | rev: 23.11.0 17 | hooks: 18 | - id: black 19 | args: [--line-length=120] 20 | - repo: https://github.com/pycqa/isort 21 | rev: 5.12.0 22 | hooks: 23 | - id: isort 24 | name: isort (python) 25 | default_language_version: 26 | python: python3.11 27 | -------------------------------------------------------------------------------- /rcs_pydantic/exceptions.py: -------------------------------------------------------------------------------- 1 | from . import errors 2 | 3 | 4 | class MessageException(Exception): 5 | def __init__(self, message: int): 6 | if errors.ErrorCodeEnum.has_value(message): 7 | self.message = errors.ErrorCodeEnum(message).value[1] 8 | elif errors.MaaPErrorCodeEnum.has_value(message): 9 | self.message = errors.MaaPErrorCodeEnum(message).value[1] 10 | elif errors.RcsBizCenterErrorCodeEnum.has_value(message): 11 | self.message = errors.RcsBizCenterErrorCodeEnum(message).value[1] 12 | elif errors.KTErrorCodeEnum.has_value(message): 13 | self.message = errors.KTErrorCodeEnum(message).value[1] 14 | else: 15 | self.message = "unknown" 16 | 17 | def __str__(self): 18 | return self.message 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joon Hwan 김준환 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Dump GitHub context 13 | env: 14 | GITHUB_CONTEXT: ${{ toJson(github) }} 15 | run: echo "$GITHUB_CONTEXT" 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: "3.8" 21 | - uses: actions/cache@v2 22 | id: cache 23 | with: 24 | path: ${{ env.pythonLocation }} 25 | key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-publish 26 | - name: Install poetry 27 | if: steps.cache.outputs.cache-hit != 'true' 28 | run: pip install poetry 29 | - name: Install Dependencies 30 | if: steps.cache.outputs.cache-hit != 'true' 31 | run: poetry install 32 | - name: Publish 33 | env: 34 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 35 | run: bash scripts/publish.sh 36 | - name: Dump GitHub context 37 | env: 38 | GITHUB_CONTEXT: ${{ toJson(github) }} 39 | run: echo "$GITHUB_CONTEXT" 40 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [pull_request] 3 | jobs: 4 | lint-test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - name: Set up Python 3.8 9 | uses: actions/setup-python@v2 10 | with: 11 | python-version: 3.8 12 | - name: Install Poetry 13 | run: | 14 | python -m pip install poetry 15 | - name: Install dependencies 16 | run: | 17 | poetry install 18 | - name: Publish 19 | run: bash scripts/lint-test.sh 20 | test: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | max-parallel: 8 24 | matrix: 25 | python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] 26 | steps: 27 | - uses: actions/checkout@v1 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install Poetry 33 | run: | 34 | python -m pip install poetry 35 | - name: Install dependencies 36 | run: | 37 | poetry install 38 | - name: Test 39 | run: | 40 | poetry run pytest --cov=rcs_pydantic tests/ --cov-report=xml 41 | - uses: codecov/codecov-action@v2 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | files: coverage.xml 45 | fail_ci_if_error: true # optional (default = false) 46 | verbose: true # optional (default = false) 47 | -------------------------------------------------------------------------------- /rcs_pydantic/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .enums import * 3 | from .errors import * 4 | from .main import * 5 | from .scheme import * 6 | 7 | __all__ = [ 8 | "RcsMessage", 9 | "RcsSMSBody", 10 | "RcsLMSBody", 11 | "RcsMMSBody", 12 | "RcsCHATBody", 13 | "RcsSMSCarouselBody", 14 | "RcsLMSCarouselBody", 15 | "RcsMMSCarouselBody", 16 | "RcsCHATCarouselBody", 17 | "LocationInfo", 18 | "ShowLocationInfo", 19 | "OpenUrlInfo", 20 | "CreateCalendarEventInfo", 21 | "CopyToClipboardInfo", 22 | "ComposeTextMessageInfo", 23 | "DialPhoneNumberInfo", 24 | "UrlActionInfo", 25 | "LocalBrowserActionInfo", 26 | "MapActionInfo", 27 | "CalendarActionInfo", 28 | "ClipboardActionInfo", 29 | "ComposeActionInfo", 30 | "DialActionInfo", 31 | "PostbackInfo", 32 | "ActionInfo", 33 | "SuggestionInfo", 34 | "ButtonInfo", 35 | "CommonInfo", 36 | "RcsInfo", 37 | "LegacyInfo", 38 | "StatusInfo", 39 | "QuerystatusInfo", 40 | "ErrorInfo", 41 | "ResponseErrorInfo", 42 | "ResponseInfo", 43 | "TextMessageInfo", 44 | "FileMessageInfo", 45 | "GeolocationPushMessage", 46 | "UserLocationInfo", 47 | "MessageInfo", 48 | "SendInfo", 49 | "TokenInfo", 50 | "LegacyErrorCodeEnum", 51 | "ErrorCodeEnum", 52 | "MaaPErrorCodeEnum", 53 | "RcsBizCenterErrorCodeEnum", 54 | "KTErrorCodeEnum", 55 | "MessageServiceTypeEnum", 56 | "ServiceTypeEnum", 57 | "LegacyServiceTypeEnum", 58 | "ScheduleTypeEnum", 59 | "ExpiryOptionEnum", 60 | "HeaderEnum", 61 | "ActionEnum", 62 | "MessageStatusEnum", 63 | "MnoInfoEnum", 64 | "BillEnum", 65 | "EventTypeEnum", 66 | "RCSMessageEnum", 67 | "MessageEnum", 68 | ] 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .DS_Store 131 | .vscode/ 132 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "rcs-pydantic" 3 | version = "2.0.0" 4 | description = "" 5 | authors = ["xncbf "] 6 | keywords = ["pydantic", "rcs", "fastapi"] 7 | homepage = "https://github.com/xncbf/rcs-pydantic" 8 | repository = "https://github.com/xncbf/rcs-pydantic" 9 | license = "MIT" 10 | readme = "README.md" 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.8" 14 | pydantic = "^2.0.0" 15 | 16 | [tool.poetry.dev-dependencies] 17 | pytest-cov = "*" 18 | pytest = "*" 19 | black = "*" 20 | mypy = "*" 21 | isort = "*" 22 | Faker = "^11.3.0" 23 | factory-boy = "^3.2.1" 24 | fastapi = "^0.109.0" 25 | uvicorn = "^0.18.3" 26 | ruff = "^0.1.14" 27 | 28 | [tool.black] 29 | line-length = 120 30 | target-version = ['py37', 'py38', 'py39', 'py310'] 31 | include = '\.pyi?$' 32 | extend-exclude = ''' 33 | # A regex preceded with ^/ will apply only to files and directories 34 | # in the root of the project. 35 | ^/foo.py # exclude a file named foo.py in the root of the project (in addition to the defaults) 36 | ''' 37 | 38 | [tool.mypy] 39 | plugins = [ 40 | "pydantic.mypy" 41 | ] 42 | python_version = '3.10' 43 | ignore_missing_imports = 'True' 44 | 45 | follow_imports = "silent" 46 | warn_redundant_casts = true 47 | warn_unused_ignores = true 48 | check_untyped_defs = true 49 | no_implicit_reexport = true 50 | 51 | 52 | [tool.pydantic-mypy] 53 | init_forbid_extra = true 54 | init_typed = true 55 | warn_required_dynamic_aliases = true 56 | 57 | [tool.isort] 58 | profile = "black" 59 | line_length = 120 60 | sections= ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] 61 | 62 | [tool.ruff] 63 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. 64 | select = [ 65 | "E", # pycodestyle errors 66 | "W", # pycodestyle warnings 67 | "F", # pyflakes 68 | "I", # isort 69 | "C", # flake8-comprehensions 70 | "B", # flake8-bugbear 71 | ] 72 | ignore = [ 73 | "B008", # do not perform function calls in argument defaults 74 | "B023", 75 | "C901" 76 | ] 77 | 78 | # Allow autofix for all enabled rules (when `--fix`) is provided. 79 | fixable = ["A", "B", "C", "D", "E", "F"] 80 | unfixable = [] 81 | 82 | # Exclude a variety of commonly ignored directories. 83 | exclude = [ 84 | ".bzr", 85 | ".direnv", 86 | ".eggs", 87 | ".git", 88 | ".hg", 89 | ".mypy_cache", 90 | ".nox", 91 | ".pants.d", 92 | ".pytype", 93 | ".ruff_cache", 94 | ".svn", 95 | ".tox", 96 | ".venv", 97 | "__pypackages__", 98 | "_build", 99 | "buck-out", 100 | "build", 101 | "dist", 102 | "node_modules", 103 | "venv", 104 | "migrations" 105 | ] 106 | 107 | # Same as Black. 108 | line-length = 120 109 | 110 | # Allow unused variables when underscore-prefixed. 111 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 112 | 113 | # Assume Python 3.8. 114 | target-version = "py38" 115 | 116 | [tool.ruff.mccabe] 117 | # Unlike Flake8, default to a complexity level of 10. 118 | max-complexity = 10 119 | 120 | # https://docs.pytest.org/en/6.2.x/reference.html 121 | [tool.pytest.ini_options] 122 | minversion = "6.2.5" 123 | 124 | [build-system] 125 | requires = ["poetry-core>=1.0.0"] 126 | build-backend = "poetry.core.masonry.api" 127 | -------------------------------------------------------------------------------- /tests/test_factory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from faker import Faker 3 | 4 | from rcs_pydantic import enums 5 | 6 | from . import factory 7 | 8 | fake = Faker() 9 | 10 | 11 | def test_rcs_sms_body_factory(): 12 | factory.RcsSMSBodyFactory() 13 | 14 | 15 | def test_rcs_lms_body_factory(): 16 | factory.RcsLMSBodyFactory() 17 | 18 | 19 | def test_rcs_mms_body_factory(): 20 | factory.RcsMMSBodyFactory() 21 | 22 | 23 | def test_rcs_chat_body_factory(): 24 | factory.RcsCHATBodyFactory() 25 | 26 | 27 | def test_location_info_factory(): 28 | factory.LocationInfoFactory() 29 | 30 | 31 | def test_show_location_info_factory(): 32 | factory.ShowLocationInfoFactory() 33 | 34 | 35 | def test_open_url_info_factory(): 36 | factory.OpenUrlInfoFactory() 37 | 38 | 39 | def test_create_calendar_event_info_factory(): 40 | factory.CreateCalendarEventInfoFactory() 41 | 42 | 43 | def test_copy_to_clipboard_info_factory(): 44 | factory.CopyToClipboardInfoFactory() 45 | 46 | 47 | def test_compose_text_message_info_factory(): 48 | factory.ComposeTextMessageInfoFactory() 49 | 50 | 51 | def test_dial_phone_number_info_factory(): 52 | factory.DialPhoneNumberInfoFactory() 53 | 54 | 55 | def test_url_action_info_factory(): 56 | factory.UrlActionInfoFactory() 57 | 58 | 59 | def test_local_browser_action_info_factory(): 60 | factory.LocalBrowserActionInfoFactory() 61 | 62 | 63 | def test_map_action_info_factory(): 64 | factory.MapActionInfoFactory() 65 | 66 | 67 | def test_calendar_action_info_factory(): 68 | factory.CalendarActionInfoFactory() 69 | 70 | 71 | def test_clipboard_action_info_factory(): 72 | factory.ClipboardActionInfoFactory() 73 | 74 | 75 | def test_compose_action_info_factory(): 76 | factory.ComposeActionInfoFactory() 77 | 78 | 79 | def test_dialer_action_info_factory(): 80 | factory.DialerActionInfoFactory() 81 | 82 | 83 | def test_postback_info_factory(): 84 | factory.PostbackInfoFactory() 85 | 86 | 87 | def test_action_info_factory(): 88 | factory.ActionInfoFactory() 89 | 90 | 91 | def test_suggestion_info_factory(): 92 | factory.SuggestionInfoFactory() 93 | 94 | 95 | def test_button_info_factory(): 96 | factory.ButtonInfoFactory() 97 | 98 | 99 | def test_common_info_factory(): 100 | factory.CommonInfoFactory() 101 | 102 | 103 | def test_rcs_info_factory(): 104 | with pytest.raises(ValueError): 105 | factory.RcsInfoFactory(header=enums.HeaderEnum.NOT_ADVERTISE, footer="080-000-0000") 106 | with pytest.raises(ValueError): 107 | factory.RcsInfoFactory(header=enums.HeaderEnum.ADVERTISE, footer=None) 108 | 109 | factory.RcsInfoFactory(header=enums.HeaderEnum.ADVERTISE, footer="080-000-0000") 110 | 111 | 112 | def test_legacy_info_factory(): 113 | factory.LegacyInfoFactory() 114 | 115 | 116 | def test_status_info_factory(): 117 | factory.StatusInfoFactory() 118 | 119 | 120 | def test_error_info_factory(): 121 | factory.ErrorInfoFactory() 122 | 123 | 124 | def test_response_error_info_factory(): 125 | factory.ResponseErrorInfoFactory() 126 | 127 | 128 | def test_token_info_factory(): 129 | factory.TokenInfoFactory() 130 | 131 | 132 | def test_response_info_factory(): 133 | factory.ResponseInfoFactory(data=factory.TokenInfoFactory().model_dump()) 134 | 135 | 136 | def test_message_info_factory(): 137 | with pytest.raises(ValueError): 138 | factory.MessageInfoFactory(eventType=enums.EventTypeEnum.RESPONSE, messageBody={"textMessage": "안녕하세요?"}) 139 | 140 | factory.MessageInfoFactory(eventType=enums.EventTypeEnum.MESSAGE, messageBody={"textMessage": "안녕하세요?"}) 141 | factory.MessageInfoFactory(eventType=enums.EventTypeEnum.MESSAGE, messageBody=None) 142 | 143 | 144 | def test_send_info_factory(): 145 | factory.SendInfoFactory() 146 | 147 | 148 | def test_file_info_factory(): 149 | factory.FileInfoFactory() 150 | 151 | 152 | def test_file_regist_info_factory(): 153 | factory.FileRegistInfoFactory() 154 | -------------------------------------------------------------------------------- /tests/test_rcs_pydantic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from faker import Faker 3 | 4 | from rcs_pydantic import enums 5 | from rcs_pydantic.errors import ErrorCodeEnum, KTErrorCodeEnum, MaaPErrorCodeEnum, RcsBizCenterErrorCodeEnum 6 | from rcs_pydantic.exceptions import MessageException 7 | from rcs_pydantic.main import RcsMessage 8 | 9 | from . import factory 10 | 11 | fake = Faker() 12 | 13 | 14 | def test_rcs_message(): 15 | rcs_message = RcsMessage( 16 | factory.MessageInfoFactory(), 17 | body=factory.RcsSMSBodyFactory(), 18 | buttons=[factory.ButtonInfoFactory()], 19 | agency_id="abc", 20 | agency_key="abc", 21 | brand_key="abc", 22 | expiry_option=enums.ExpiryOptionEnum.AFTER_SETTING_TIMES, 23 | header=enums.HeaderEnum.ADVERTISE, 24 | footer="010-0000-0000", 25 | cdr_id="abc", 26 | copy_allowed=True, 27 | message_group_id="abc", 28 | ) 29 | assert rcs_message.send_info 30 | 31 | 32 | def test_rcs_chat_message(): 33 | rcs_message = RcsMessage( 34 | factory.MessageInfoFactory(), 35 | body=factory.RcsCHATBodyFactory(), 36 | buttons=[factory.ButtonInfoFactory()], 37 | agency_id="abc", 38 | agency_key="abc", 39 | brand_key="abc", 40 | expiry_option=enums.ExpiryOptionEnum.AFTER_SETTING_TIMES, 41 | header=enums.HeaderEnum.ADVERTISE, 42 | footer="010-0000-0000", 43 | cdr_id="abc", 44 | copy_allowed=True, 45 | service_type=enums.ServiceTypeEnum.CHAT, 46 | chips=[factory.SuggestionInfoFactory()], 47 | ) 48 | assert rcs_message.send_info 49 | 50 | 51 | def test_rcs_message_with_empty_button(): 52 | rcs_message = RcsMessage( 53 | factory.MessageInfoFactory(), 54 | body=factory.RcsSMSBodyFactory(), 55 | buttons=[{}], 56 | agency_id="abc", 57 | agency_key="abc", 58 | brand_key="abc", 59 | expiry_option=enums.ExpiryOptionEnum.AFTER_SETTING_TIMES, 60 | header=enums.HeaderEnum.ADVERTISE, 61 | footer="010-0000-0000", 62 | cdr_id="abc", 63 | copy_allowed=True, 64 | ) 65 | assert rcs_message.send_info 66 | 67 | 68 | def test_tuple_enum_has_value(): 69 | assert ErrorCodeEnum.has_value(ErrorCodeEnum.MISSING_AUTHORIZATION_HEADER.value[0]) 70 | 71 | 72 | def test_exception_error_message(): 73 | try: 74 | raise MessageException(ErrorCodeEnum.MISSING_AUTHORIZATION_HEADER.value[0]) 75 | except MessageException as e: 76 | assert e.message == "Valid access token in Authorization header is required for RESTful API calls." 77 | assert str(e) == "Valid access token in Authorization header is required for RESTful API calls." 78 | 79 | 80 | def test_error_code_message_exception(): 81 | with pytest.raises(MessageException): 82 | raise MessageException(ErrorCodeEnum.MISSING_AUTHORIZATION_HEADER.value[0]) 83 | 84 | 85 | def test_maap_error_code_message_exception(): 86 | with pytest.raises(MessageException): 87 | raise MessageException(MaaPErrorCodeEnum.ACTION_BUTTON_PERMISSION_ERROR.value[0]) 88 | 89 | 90 | def test_rcsbiz_error_code_message_exception(): 91 | with pytest.raises(MessageException): 92 | raise MessageException(RcsBizCenterErrorCodeEnum.MISSING_AUTHORIZATION_HEADER.value[0]) 93 | 94 | 95 | def test_kt_error_code_message_exception(): 96 | with pytest.raises(MessageException): 97 | raise MessageException(KTErrorCodeEnum.MISSING_AUTHORIZATION_HEADER.value[0]) 98 | 99 | 100 | def test_unknown_error_code_message_exception(): 101 | with pytest.raises(MessageException): 102 | raise MessageException(123) 103 | 104 | 105 | def test_rcs_legacy_message(): 106 | rcs_message = RcsMessage( 107 | factory.MessageInfoFactory(), 108 | body=factory.RcsSMSBodyFactory(), 109 | buttons=[factory.ButtonInfoFactory()], 110 | agency_id="abc", 111 | agency_key="abc", 112 | brand_key="abc", 113 | expiry_option=enums.ExpiryOptionEnum.AFTER_SETTING_TIMES, 114 | header=enums.HeaderEnum.ADVERTISE, 115 | footer="010-0000-0000", 116 | cdr_id="abc", 117 | copy_allowed=True, 118 | legacy=factory.LegacyInfoFactory(), 119 | ) 120 | assert rcs_message.send_info 121 | -------------------------------------------------------------------------------- /rcs_pydantic/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | 3 | 4 | class MessageServiceTypeEnum(Enum): 5 | RCS: str = "rcs" 6 | RCS_LEGACY: str = "rcs, legacy" # rcs 부달시, xMS로fallback전송 7 | 8 | 9 | class ServiceTypeEnum(Enum): 10 | SMS: str = "RCSSMS" 11 | LMS: str = "RCSLMS" 12 | MMS: str = "RCSMMS" 13 | TMPL: str = "RCSTMPL" 14 | ITMPL: str = "RCSITMPL" 15 | CHAT: str = "RCSCHAT" 16 | 17 | 18 | class LegacyServiceTypeEnum(Enum): 19 | SMS: str = "SMS" 20 | LMS: str = "LMS" 21 | MMS: str = "MMS" 22 | 23 | 24 | class ScheduleTypeEnum(IntEnum): 25 | IMMEDIATE: int = 0 26 | 27 | 28 | class ExpiryOptionEnum(IntEnum): 29 | AFTER_THREE_DAYS: int = 1 30 | AFTER_SETTING_TIMES: int = 2 31 | 32 | 33 | class HeaderEnum(Enum): 34 | """ 35 | 헤더 광고에 대한 정의 36 | 0: 광고 넣지않음 37 | 1: 광고 넣음 38 | """ 39 | 40 | NOT_ADVERTISE: str = "0" 41 | ADVERTISE: str = "1" 42 | 43 | 44 | class ActionEnum(Enum): 45 | URL_ACTION: str = "urlAction" # 단말기에 기본 웹 브라우저로 설정된 앱을 통해서, 웹페이지로 이동할 수있습니다 46 | LOCAL_BROWSER_ACTION: str = "localBrowserAction" # 단말기의 메시지 앱 내부 브라우저를 통해 웹페이지로 이동할 수 있습니다 # noqa: E501 47 | MAP_ACTION: str = "mapAction" # 미리 지정된 위치를 보여주거나 사용자의 현재 위치를 서버로 전송 할 수 있습니다. 48 | CALENDAR_ACTION: str = "calendarAction" # 사용자의 캘린더에 특정 일정을 등록 할 수 있습니다. 49 | CLIPBOARD_ACTION: str = "clipboardAction" # 특정 문구를 사용자 단말이 자동으로 복사 할 수 있게 합니다 50 | COMPOSE_ACTION: str = "composeAction" # 다른 번호로 메시지를 보낼 수 있도록 대화방을 엽니다. 51 | DIALER_ACTION: str = "dialerAction" # 특정 전화번호로 전화를 걸 수 있습니다. 52 | REPLY: str = "reply" # postbackData 및 displayText 등의 값을 양방향 MO 메시지로 전송합니다. 53 | 54 | 55 | class MessageStatusEnum(Enum): 56 | SUCCESS: str = "success" 57 | FAIL: str = "fail" 58 | 59 | 60 | class MnoInfoEnum(Enum): 61 | KT: str = "KT" 62 | SKT: str = "SKT" 63 | LGU: str = "LGU" 64 | 65 | 66 | class BillEnum(IntEnum): 67 | FREE: int = 0 68 | CHARGE: int = 1 69 | 70 | 71 | class EventTypeEnum(Enum): 72 | MESSAGE: str = "message" 73 | RESPONSE: str = "response" 74 | NEW_USER: str = "newUser" 75 | 76 | 77 | class RCSMessageEnum(Enum): 78 | SMS: str = "SCS00000" # 기본 말풍선 (SMS) 79 | LMS: str = "SCL00000" # 텍스트 카드 (LMS) 80 | CAROUSEL_MEDIUM_2: str = "CCwMhM0200" # 슬라이드형(Medium, 2장) 81 | CAROUSEL_MEDIUM_3: str = "CCwMhM0300" # 슬라이드형(Medium, 3장) 82 | CAROUSEL_MEDIUM_4: str = "CCwMhM0400" # 슬라이드형(Medium, 4장) 83 | CAROUSEL_MEDIUM_5: str = "CCwMhM0500" # 슬라이드형(Medium, 5장) 84 | CAROUSEL_MEDIUM_6: str = "CCwMhM0600" # 슬라이드형(Medium, 6장) 85 | CAROUSEL_SMALL_2: str = "CCwShS0200" # 슬라이드형(Small, 2장) 86 | CAROUSEL_SMALL_3: str = "CCwShS0300" # 슬라이드형(Small, 3장) 87 | CAROUSEL_SMALL_4: str = "CCwShS0400" # 슬라이드형(Small, 4장) 88 | CAROUSEL_SMALL_5: str = "CCwShS0500" # 슬라이드형(Small, 5장) 89 | CAROUSEL_SMALL_6: str = "CCwShS0600" # 슬라이드형(Small, 6장) 90 | STANDALONE_MEDIA_TOP_TALL: str = "SCwThT00" # 세로형(Tall) 91 | STANDALONE_MEDIA_TOP_MEDIUM: str = "SCwThM00" # 세로형(Medium) 92 | 93 | 94 | class MessageEnum(Enum): 95 | SMS: str = "SS000000" # 기본 말풍선 (SMS) 96 | LMS: str = "SL000000" # 텍스트 카드 (LMS) 97 | STANDALONE_MEDIA_TOP_TALL: str = "SMwThT00" # 세로형(Tall) 98 | STANDALONE_MEDIA_TOP_MEDIUM: str = "SMwThM00" # 세로형(Medium) 99 | CAROUSEL_MEDIUM_2: str = "CMwMhM0200" # 슬라이드형(Medium, 2장) 100 | CAROUSEL_MEDIUM_3: str = "CMwMhM0300" # 슬라이드형(Medium, 3장) 101 | CAROUSEL_MEDIUM_4: str = "CMwMhM0400" # 슬라이드형(Medium, 4장) 102 | CAROUSEL_MEDIUM_5: str = "CMwMhM0500" # 슬라이드형(Medium, 5장) 103 | CAROUSEL_MEDIUM_6: str = "CMwMhM0600" # 슬라이드형(Medium, 6장) 104 | CAROUSEL_SMALL_2: str = "CMwShS0200" # 슬라이드형(Small, 2장) 105 | CAROUSEL_SMALL_3: str = "CMwShS0300" # 슬라이드형(Small, 3장) 106 | CAROUSEL_SMALL_4: str = "CMwShS0400" # 슬라이드형(Small, 4장) 107 | CAROUSEL_SMALL_5: str = "CMwShS0500" # 슬라이드형(Small, 5장) 108 | CAROUSEL_SMALL_6: str = "CMwShS0600" # 슬라이드형(Small, 6장) 109 | HIGHLIGHTED_IMAGE_N_TITLE_LONG: str = "OMHITV0001" # 이미지 & 타이틀 강조형 (3:4) 110 | HIGHLIGHTED_IMAGE_N_TITLE_SQUARE: str = "OMHITS0001" # 이미지 & 타이틀 강조형 (1:1) 111 | HIGHLIGHTED_IMAGE_LONG: str = "OMHIMV0001" # 이미지 강조형 (3:4) 112 | HIGHLIGHTED_IMAGE_SQUARE: str = "OMHIMS0001" # 이미지 강조형 (1:1) 113 | THUMBNAIL_VERTICAL: str = "OMTBNV0001" # 썸네일형 (세로) 114 | THUMBNAIL_HORIZONTAL: str = "OMTBNS0001" # 썸네일형 (가로) 115 | SNS_RACTANGLE: str = "OMSNSS0001" # SNS형 (가로) 116 | SNS_SQUARE: str = "OMSNSH0001" # SNS형 (세로) 117 | 118 | 119 | class FileUsageTypeEnum(Enum): 120 | SEND: str = "send" 121 | 122 | 123 | class FileUsageServiceEnum(Enum): 124 | RCS: str = "RCS" 125 | MMS: str = "MMS" 126 | 127 | 128 | class FileStatusEnum(Enum): 129 | READY: str = "ready" 130 | EXPIRED: str = "expired" 131 | -------------------------------------------------------------------------------- /rcs_pydantic/main.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Optional, Union 3 | 4 | from rcs_pydantic import enums 5 | 6 | from . import scheme 7 | 8 | 9 | class RcsMessage: 10 | def __init__( 11 | self, 12 | message_info: scheme.MessageInfo, 13 | body: Union[ 14 | scheme.RcsSMSBody, 15 | scheme.RcsLMSBody, 16 | scheme.RcsMMSBody, 17 | scheme.RcsCHATBody, 18 | scheme.RcsSMSCarouselBody, 19 | scheme.RcsLMSCarouselBody, 20 | scheme.RcsMMSCarouselBody, 21 | scheme.RcsCHATCarouselBody, 22 | dict, 23 | ], 24 | agency_key: str, 25 | brand_key: str, 26 | agency_id: Optional[str] = None, 27 | message_base_id: Union[enums.MessageEnum, enums.RCSMessageEnum] = enums.MessageEnum.SMS, 28 | service_type: enums.ServiceTypeEnum = enums.ServiceTypeEnum.SMS, 29 | expiry_option: Optional[enums.ExpiryOptionEnum] = None, 30 | header: enums.HeaderEnum = enums.HeaderEnum.NOT_ADVERTISE, 31 | footer: Optional[str] = None, 32 | cdr_id: Optional[str] = None, 33 | copy_allowed: Optional[bool] = None, 34 | buttons: Optional[list] = None, 35 | chips: Optional[list] = None, 36 | legacy: Optional[scheme.LegacyInfo] = None, 37 | message_group_id: Optional[str] = None, 38 | ): 39 | self.message_info = message_info 40 | self.agency_id = agency_id 41 | self.agency_key = agency_key 42 | self.brand_key = brand_key 43 | self.message_base_id = message_base_id 44 | self.service_type = service_type 45 | self.expiry_option = expiry_option 46 | self.header = header 47 | self.footer = footer 48 | self.cdr_id = cdr_id 49 | self.copy_allowed = copy_allowed 50 | self.body = body 51 | self.buttons = buttons 52 | self.chips = chips 53 | self.legacy = legacy 54 | self.message_group_id = message_group_id 55 | self.send_info = self.make_send_info( 56 | common=scheme.CommonInfo(**self.make_common_info(message_info)), 57 | rcs=scheme.RcsInfo(**self.make_rcs_info(message_info)), 58 | ) 59 | 60 | def make_send_info(self, common, rcs): 61 | if self.legacy: 62 | return scheme.SendInfo( 63 | common=common, 64 | rcs=rcs, 65 | legacy=self.legacy, 66 | ) 67 | 68 | else: 69 | return scheme.SendInfo( 70 | common=common, 71 | rcs=rcs, 72 | ) 73 | 74 | def make_common_info(self, message_info: scheme.MessageInfo) -> dict: 75 | if self.legacy: 76 | msg_service_type = enums.MessageServiceTypeEnum.RCS_LEGACY 77 | else: 78 | msg_service_type = enums.MessageServiceTypeEnum.RCS 79 | 80 | if self.message_group_id: 81 | return scheme.CommonInfo( 82 | msgId=str(uuid.uuid4()), 83 | userContact=str(message_info.userContact), 84 | scheduleType=enums.ScheduleTypeEnum.IMMEDIATE, 85 | msgServiceType=msg_service_type, 86 | msgGroupId=self.message_group_id, 87 | ).model_dump(exclude_none=True) 88 | else: 89 | return scheme.CommonInfo( 90 | msgId=str(uuid.uuid4()), 91 | userContact=str(message_info.userContact), 92 | scheduleType=enums.ScheduleTypeEnum.IMMEDIATE, 93 | msgServiceType=msg_service_type, 94 | ).model_dump(exclude_none=True) 95 | 96 | def make_rcs_info(self, message_info: scheme.MessageInfo) -> dict: 97 | rcs_info = scheme.RcsInfo( 98 | chatbotId=message_info.chatbotId, 99 | messagebaseId=self.message_base_id, 100 | serviceType=self.service_type, 101 | header=self.header, 102 | body=self.body, 103 | agencyKey=self.agency_key, 104 | brandKey=self.brand_key, 105 | ) 106 | if self.agency_id: 107 | rcs_info.agencyId = self.agency_id 108 | if self.expiry_option: 109 | rcs_info.expiryOption = self.expiry_option 110 | if self.service_type == enums.ServiceTypeEnum.CHAT: 111 | rcs_info.expiryOption = enums.ExpiryOptionEnum.AFTER_SETTING_TIMES 112 | if self.footer: 113 | rcs_info.footer = self.footer 114 | if self.cdr_id: 115 | rcs_info.cdrId = self.cdr_id 116 | if self.copy_allowed: 117 | rcs_info.copyAllowed = self.copy_allowed 118 | if self.buttons: 119 | rcs_info.buttons = self.buttons 120 | if self.chips: 121 | rcs_info.chipList = self.chips 122 | if message_info.replyId: 123 | rcs_info.replyId = message_info.replyId 124 | return rcs_info.model_dump(exclude_none=True) 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RCS-PYDANTIC 2 | 3 |

4 | 5 | Test 6 | 7 | 8 | Coverage 9 | 10 | 11 | Package version 12 | 13 | 14 | Supported Python versions 15 | 16 |

17 | 18 | - [RCS-PYDANTIC](#rcs-pydantic) 19 | - [Introduce](#introduce) 20 | - [Installation](#installation) 21 | - [Dependency](#dependency) 22 | - [Quick start](#quick-start) 23 | - [제공되는 항목](#제공되는-항목) 24 | - [제공되는 데이터 pydantic 모델](#제공되는-데이터-pydantic-모델) 25 | - [제공되는 데이터 관련 Enum](#제공되는-데이터-관련-enum) 26 | - [제공되는 에러 코드 Enum](#제공되는-에러-코드-enum) 27 | - [Features](#features) 28 | - [RcsMessage](#rcsmessage) 29 | - [MessageException](#messageexception) 30 | - [Contribution](#contribution) 31 | 32 | ## Introduce 33 | 34 | 한국 통신사 rcs 를 위한 pydantic 모델 35 | 36 | [fastapi](https://github.com/tiangolo/fastapi) 또는 [django-ninja](https://github.com/vitalik/django-ninja) 와 함께 사용할 때 유용합니다. 37 | 38 | ## Installation 39 | 40 | ```sh 41 | pip install rcs-pydantic 42 | ``` 43 | 44 | ## Dependency 45 | 46 | - python3.x (3.8 이상) 47 | - pydantic 48 | 49 | ## Quick start 50 | 51 | ```py 52 | from rcs_pydantic import MessageInfo, RcsMessage 53 | 54 | message_info = { 55 | "replyId": "B01RDSFR.KcNNLk67ui.FDSAF432153214", 56 | "eventType":"newUser", 57 | "displayText": "안녕", 58 | "userContact":"01012341234", 59 | "chatbotId":"0212351235", 60 | "timestamp": "2020-03-03T04:43:55.867+09" 61 | } 62 | 63 | rcs = { 64 | "message_base_id": "SS000000", 65 | "service_type": "RCSSMS", 66 | "agency_id": "", 67 | "body": { 68 | "title": "타이틀", 69 | "description": "일반 RCSSMS 테스트 메시지 입니다." 70 | } 71 | } 72 | 73 | 74 | rcs_message = RcsMessage(message_info=MessageInfo(**message_info), **rcs) 75 | ``` 76 | 77 | ```sh 78 | >>> print(rcs_message.send_info) 79 | common=CommonInfo( 80 | msgId='4be0072f-0f05-4b3a-adc8-90d7ef309c53', 81 | userContact='01012341234', 82 | scheduleType=, 83 | msgServiceType= 84 | ) 85 | rcs=RcsInfo( 86 | chatbotId='0212351235', 87 | agencyId='', 88 | messagebaseId='SS000000', 89 | serviceType=, 90 | expiryOption=, 91 | header=, 92 | copyAllowed=True, 93 | body=RcsSMSBody(title='타이틀', description='일반 RCSSMS 테스트 메시지 입니다.'), 94 | agencyKey='' 95 | brandKey='' 96 | ) 97 | >>> 98 | ``` 99 | 100 | ## 제공되는 항목 101 | 102 | 국내 통신사 RCS 문서에서 제공되는 모든 데이터를 pydandic 모델로써 지원합니다. 103 | 104 | ### 제공되는 데이터 pydantic 모델 105 | 106 | ```python 107 | RcsSMSBody 108 | RcsLMSBody 109 | RcsMMSBody 110 | RcsCHATBody 111 | RcsSMSCarouselBody 112 | RcsLMSCarouselBody 113 | RcsMMSCarouselBody 114 | RcsCHATCarouselBody 115 | LocationInfo 116 | ShowLocationInfo 117 | OpenUrlInfo 118 | CreateCalendarEventInfo 119 | CopyToClipboardInfo 120 | ComposeTextMessageInfo 121 | DialPhoneNumberInfo 122 | UrlActionInfo 123 | LocalBrowserActionInfo 124 | MapActionInfo 125 | CalendarActionInfo 126 | ClipboardActionInfo 127 | ComposeActionInfo 128 | DialActionInfo 129 | PostbackInfo 130 | ActionInfo 131 | SuggestionInfo 132 | ButtonInfo 133 | CommonInfo 134 | RcsInfo 135 | LegacyInfo 136 | StatusInfo 137 | QuerystatusInfo 138 | ErrorInfo 139 | ResponseErrorInfo 140 | ResponseInfo 141 | TextMessageInfo 142 | FileMessageInfo 143 | GeolocationPushMessage 144 | UserLocationInfo 145 | MessageInfo 146 | SendInfo 147 | TokenInfo 148 | ``` 149 | 150 | ### 제공되는 데이터 관련 Enum 151 | 152 | ```python 153 | EventTypeEnum 154 | RCSMessageEnum 155 | MessageEnum 156 | MessageStatusEnum 157 | MnoInfoEnum 158 | BillEnum 159 | MessageServiceTypeEnum 160 | ServiceTypeEnum 161 | LegacyServiceTypeEnum 162 | ScheduleTypeEnum 163 | ExpiryOptionEnum 164 | HeaderEnum 165 | ActionEnum 166 | ``` 167 | 168 | ### 제공되는 에러 코드 Enum 169 | 170 | ```python 171 | ErrorCodeEnum 172 | MaaPErrorCodeEnum 173 | RcsBizCenterErrorCodeEnum 174 | KTErrorCodeEnum 175 | LegacyErrorCodeEnum 176 | ``` 177 | 178 | ## Features 179 | 180 | ### RcsMessage 181 | 182 | `RcsMessage` 클래스는 서버로 수신된 `MessageInfo` 메세지 모델을 기반으로 메세지 전송을 위한 `SendInfo` 모델을 만듭니다. 183 | 184 | ```py 185 | from rcs_pydantic import MessageInfo, RcsMessage 186 | 187 | message_info = { 188 | "replyId": "B01RDSFR.KcNNLk67ui.FDSAF432153214", 189 | "eventType":"newUser", 190 | "displayText": "안녕", 191 | "userContact":"01012341234", 192 | "chatbotId":"0212351235", 193 | "timestamp": "2020-03-03T04:43:55.867+09" 194 | } 195 | 196 | rcs = { 197 | "message_base_id": "SS000000", 198 | "service_type": "RCSSMS", 199 | "agency_id": "", 200 | "body": { 201 | "title": "타이틀", 202 | "description": "일반 RCSSMS 테스트 메시지 입니다." 203 | } 204 | } 205 | 206 | 207 | rcs_message = RcsMessage(message_info=MessageInfo(**message_info), **rcs) 208 | ``` 209 | 210 | ### MessageException 211 | 212 | `MessageException` 예외 클래스는 제공되는 모든 에러 코드 Enum 을 포함하는 예외 클래스입니다. 213 | 214 | 다음과 같이 여러 Enum 코드중 한가지를 메세지로 반환합니다. 215 | 216 | ```python 217 | from rcs_pydantic.errors import ErrorCodeEnum 218 | from rcs_pydantic.exceptions import MessageException 219 | try: 220 | raise MessageException(ErrorCodeEnum.MISSING_AUTHORIZATION_HEADER.value[0]) 221 | except MessageException as e: 222 | print(f"ERROR MESSAGE: {e}") 223 | 224 | ERROR MESSAGE: Valid access token in Authorization header is required for RESTful API calls. 225 | >>> 226 | ``` 227 | 228 | 다음과 같이 `has_value` 를 통해 특정 `Enum` 에 포함된 에러인지 확인할 수 있습니다. 229 | 230 | ```python 231 | >>> from rcs_pydantic.errors import ErrorCodeEnum 232 | ... ErrorCodeEnum.has_value(40003) 233 | True 234 | >>> ErrorCodeEnum.has_value(11111) 235 | False 236 | ``` 237 | 238 | ## Contribution 239 | 240 | 이 프로젝트는 기여를 환영합니다! 241 | 242 | 패치를 제출하기 전에 issue 티켓을 먼저 제출해주세요. 243 | 244 | Pull request 는 `main` 브랜치로 머지되며 항상 사용 가능한 상태로 유지해야 합니다. 245 | 246 | 모든 테스트 코드를 통과한 뒤 리뷰한 후 머지됩니다. 247 | -------------------------------------------------------------------------------- /tests/factory.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | 4 | import factory 5 | from faker import Faker 6 | 7 | from rcs_pydantic import enums, errors, scheme 8 | 9 | fake = Faker() 10 | 11 | 12 | class RcsSMSBodyFactory(factory.Factory): 13 | class Meta: 14 | model = scheme.RcsSMSBody 15 | 16 | title: str = factory.Faker("name") 17 | description: str = factory.Faker("name") 18 | 19 | 20 | class RcsLMSBodyFactory(factory.Factory): 21 | class Meta: 22 | model = scheme.RcsLMSBody 23 | 24 | title: str = factory.Faker("name") 25 | description: str = factory.Faker("name") 26 | 27 | 28 | class RcsMMSBodyFactory(factory.Factory): 29 | class Meta: 30 | model = scheme.RcsMMSBody 31 | 32 | title: str = factory.Faker("name") 33 | description: str = factory.Faker("name") 34 | 35 | 36 | class RcsCHATBodyFactory(factory.Factory): 37 | class Meta: 38 | model = scheme.RcsCHATBody 39 | 40 | title: str = factory.Faker("name") 41 | description: str = factory.Faker("name") 42 | 43 | 44 | class LocationInfoFactory(factory.Factory): 45 | class Meta: 46 | model = scheme.LocationInfo 47 | 48 | latitude: float = factory.Faker("latitude") 49 | longitude: float = factory.Faker("longitude") 50 | label: str = factory.Faker("name") 51 | # query: str = factory.Faker("name") 52 | 53 | 54 | class ShowLocationInfoFactory(factory.Factory): 55 | class Meta: 56 | model = scheme.ShowLocationInfo 57 | 58 | fallbackUrl: str = factory.Faker("url") 59 | location: scheme.LocationInfo = factory.SubFactory(LocationInfoFactory) 60 | 61 | 62 | class OpenUrlInfoFactory(factory.Factory): 63 | class Meta: 64 | model = scheme.OpenUrlInfo 65 | 66 | url: str = factory.Faker("url") 67 | 68 | 69 | class CreateCalendarEventInfoFactory(factory.Factory): 70 | class Meta: 71 | model = scheme.CreateCalendarEventInfo 72 | 73 | title: str = factory.Faker("name") 74 | description: str = factory.Faker("name") 75 | startTime: str = factory.Faker("iso8601") 76 | endTime: str = factory.Faker("iso8601") 77 | 78 | 79 | class CopyToClipboardInfoFactory(factory.Factory): 80 | class Meta: 81 | model = scheme.CopyToClipboardInfo 82 | 83 | text: str = factory.Faker("text") 84 | 85 | 86 | class ComposeTextMessageInfoFactory(factory.Factory): 87 | class Meta: 88 | model = scheme.ComposeTextMessageInfo 89 | 90 | phoneNumber: str = factory.Faker("phone_number") 91 | text: str = factory.Faker("text") 92 | 93 | 94 | class DialPhoneNumberInfoFactory(factory.Factory): 95 | class Meta: 96 | model = scheme.DialPhoneNumberInfo 97 | 98 | phoneNumber: str = factory.Faker("phone_number") 99 | 100 | 101 | class UrlActionInfoFactory(factory.Factory): 102 | class Meta: 103 | model = scheme.UrlActionInfo 104 | 105 | openUrl: scheme.OpenUrlInfo = factory.SubFactory(OpenUrlInfoFactory) 106 | 107 | 108 | class LocalBrowserActionInfoFactory(factory.Factory): 109 | class Meta: 110 | model = scheme.LocalBrowserActionInfo 111 | 112 | openUrl: scheme.OpenUrlInfo = factory.SubFactory(OpenUrlInfoFactory) 113 | 114 | 115 | class MapActionInfoFactory(factory.Factory): 116 | class Meta: 117 | model = scheme.MapActionInfo 118 | 119 | showLocation: scheme.ShowLocationInfo = factory.SubFactory(ShowLocationInfoFactory) 120 | 121 | 122 | class CalendarActionInfoFactory(factory.Factory): 123 | class Meta: 124 | model = scheme.CalendarActionInfo 125 | 126 | createCalendarEvent: scheme.CreateCalendarEventInfo = factory.SubFactory(CreateCalendarEventInfoFactory) 127 | 128 | 129 | class ClipboardActionInfoFactory(factory.Factory): 130 | class Meta: 131 | model = scheme.ClipboardActionInfo 132 | 133 | copyToClipboard: scheme.CopyToClipboardInfo = factory.SubFactory(CopyToClipboardInfoFactory) 134 | 135 | 136 | class ComposeActionInfoFactory(factory.Factory): 137 | class Meta: 138 | model = scheme.ComposeActionInfo 139 | 140 | composeTextMessage: scheme.ComposeTextMessageInfo = factory.SubFactory(ComposeTextMessageInfoFactory) 141 | 142 | 143 | class DialerActionInfoFactory(factory.Factory): 144 | class Meta: 145 | model = scheme.DialerActionInfo 146 | 147 | dialPhoneNumber: scheme.DialPhoneNumberInfo = factory.SubFactory(DialPhoneNumberInfoFactory) 148 | 149 | 150 | class PostbackInfoFactory(factory.Factory): 151 | class Meta: 152 | model = scheme.PostbackInfo 153 | 154 | data: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=200)[:2048]) 155 | 156 | 157 | class ActionInfoFactory(factory.Factory): 158 | class Meta: 159 | model = scheme.ActionInfo 160 | 161 | urlAction: scheme.UrlActionInfo = factory.SubFactory(UrlActionInfoFactory) 162 | displayText: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=20)[:200]) 163 | postback: scheme.PostbackInfo = factory.SubFactory(PostbackInfoFactory) 164 | 165 | 166 | class SuggestionInfoFactory(factory.Factory): 167 | class Meta: 168 | model = scheme.SuggestionInfo 169 | 170 | action: scheme.ActionInfo = factory.SubFactory(ActionInfoFactory) 171 | 172 | 173 | class ButtonInfoFactory(factory.Factory): 174 | class Meta: 175 | model = scheme.ButtonInfo 176 | 177 | suggestions: List[scheme.SuggestionInfo] = factory.List( 178 | [factory.SubFactory(SuggestionInfoFactory) for _ in range(2)] 179 | ) 180 | 181 | 182 | class CommonInfoFactory(factory.Factory): 183 | class Meta: 184 | model = scheme.CommonInfo 185 | 186 | msgId: str = factory.Faker("uuid4") 187 | userContact: str = factory.Faker("phone_number") 188 | scheduleType: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.ScheduleTypeEnum)) 189 | msgGroupId: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=10)[:20]) 190 | msgServiceType: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.MessageServiceTypeEnum)) 191 | 192 | 193 | class RcsInfoFactory(factory.Factory): 194 | class Meta: 195 | model = scheme.RcsInfo 196 | 197 | chatbotId: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=10)[:40]) 198 | agencyId: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=10)[:20]) 199 | agencyKey: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=10)[:20]) 200 | brandKey: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=10)[:20]) 201 | messagebaseId: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.MessageEnum)) 202 | serviceType: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.ServiceTypeEnum)) 203 | expiryOption: int = 2 204 | header: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.HeaderEnum)) 205 | body: scheme.RcsSMSBody = factory.SubFactory(RcsSMSBodyFactory) 206 | 207 | 208 | class LegacyInfoFactory(factory.Factory): 209 | class Meta: 210 | model = scheme.LegacyInfo 211 | 212 | serviceType: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.LegacyServiceTypeEnum)) 213 | callback: str = factory.Faker("name") 214 | subject: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=20)[:50]) 215 | msg: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=200)[:4000]) 216 | contentCount: int = 0 217 | 218 | 219 | class StatusInfoFactory(factory.Factory): 220 | class Meta: 221 | model = scheme.StatusInfo 222 | 223 | rcsId: str = factory.Faker("name") 224 | msgId: str = factory.Faker("name") 225 | userContact: str = factory.Faker("phone_number") 226 | status: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.MessageStatusEnum)) 227 | serviceType: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.ServiceTypeEnum)) 228 | mnoInfo: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.MnoInfoEnum)) 229 | 230 | @factory.lazy_attribute 231 | def sentTime(self) -> str: 232 | t = datetime.now() 233 | s: str = t.strftime("%Y-%m-%dT%H:%M:%S.%f") 234 | s = s[:-3] 235 | return s + "+09" 236 | 237 | @factory.lazy_attribute 238 | def timestamp(self) -> str: 239 | t = datetime.now() 240 | s: str = t.strftime("%Y-%m-%dT%H:%M:%S.%f") 241 | s = s[:-3] 242 | return s + "+09" 243 | 244 | 245 | class ErrorInfoFactory(factory.Factory): 246 | class Meta: 247 | model = scheme.ErrorInfo 248 | 249 | code: str = factory.LazyAttribute( 250 | lambda n: str(fake.random_element(elements=[x.value[0] for x in errors.ErrorCodeEnum])) 251 | ) 252 | message: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=20)[:50]) 253 | 254 | 255 | class ResponseErrorInfoFactory(factory.Factory): 256 | class Meta: 257 | model = scheme.ResponseErrorInfo 258 | 259 | status: str = "403" 260 | error: scheme.ErrorInfo = factory.SubFactory(ErrorInfoFactory) 261 | 262 | 263 | class TokenInfoFactory(factory.Factory): 264 | class Meta: 265 | model = scheme.TokenInfo 266 | 267 | rcsId: str = factory.Faker("uuid4") 268 | rcsSecret: str = factory.Faker("password") 269 | grantType: str = "clientCredentials" 270 | 271 | 272 | class ResponseInfoFactory(factory.Factory): 273 | class Meta: 274 | model = scheme.ResponseInfo 275 | 276 | status: str = "200" 277 | 278 | 279 | class MessageInfoFactory(factory.Factory): 280 | class Meta: 281 | model = scheme.MessageInfo 282 | 283 | replyId: str = factory.Faker("uuid4") 284 | eventType: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.EventTypeEnum)) 285 | userContact: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=10)[:40]) 286 | chatbotId: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=10)[:40]) 287 | 288 | @factory.lazy_attribute 289 | def timestamp(self) -> str: 290 | t = datetime.now() 291 | s: str = t.strftime("%Y-%m-%dT%H:%M:%S.%f") 292 | s = s[:-3] 293 | return s + "+09" 294 | 295 | 296 | class SendInfoFactory(factory.Factory): 297 | class Meta: 298 | model = scheme.SendInfo 299 | 300 | common: scheme.CommonInfo = factory.SubFactory(CommonInfoFactory) 301 | rcs: scheme.RcsInfo = factory.SubFactory(RcsInfoFactory) 302 | 303 | 304 | class FileRegistInfoFactory(factory.Factory): 305 | fileId: str = factory.Faker("uuid4") 306 | usageType: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.FileUsageTypeEnum)) 307 | usageService: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.FileUsageServiceEnum)) 308 | mimeType: str = fake.mime_type(category="image") 309 | file: bytes = b"""\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4 \ 310 | \x9as\x00\x00\x00\x16IDATx\x9cc\x9ccy\x97\x81\x81\x81i\xe5\x8d\x0b\x0c\x0c\x0c\x00\x1f\xaf\x04 \ 311 | \x07\xa1\xbdi\xdd\x00\x00\x00\x00IEND\xaeB`\x82""" 312 | description: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=10)[:20]) 313 | 314 | class Meta: 315 | model = scheme.FileRegistInfo 316 | 317 | 318 | class FileInfoFactory(factory.Factory): 319 | usageType: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.FileUsageTypeEnum)) 320 | usageService: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.FileUsageServiceEnum)) 321 | mimeType: str = fake.mime_type(category="image") 322 | status: str = factory.LazyAttribute(lambda n: fake.random_element(elements=enums.FileStatusEnum)) 323 | 324 | @factory.lazy_attribute 325 | def expiryDate(self) -> str: 326 | t = datetime.now() 327 | s: str = t.strftime("%Y-%m-%dT%H:%M:%S.%f") 328 | s = s[:-3] 329 | return s + "+09" 330 | 331 | url: str = factory.Faker("url") 332 | 333 | class Meta: 334 | model = scheme.FileInfo 335 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "annotated-types" 3 | version = "0.6.0" 4 | description = "Reusable constraint types to use with typing.Annotated" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.8" 8 | 9 | [package.dependencies] 10 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} 11 | 12 | [[package]] 13 | name = "anyio" 14 | version = "4.2.0" 15 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 16 | category = "dev" 17 | optional = false 18 | python-versions = ">=3.8" 19 | 20 | [package.dependencies] 21 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 22 | idna = ">=2.8" 23 | sniffio = ">=1.1" 24 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 25 | 26 | [package.extras] 27 | doc = ["packaging", "Sphinx (>=7)", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] 28 | test = ["anyio", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 29 | trio = ["trio (>=0.23)"] 30 | 31 | [[package]] 32 | name = "black" 33 | version = "23.12.1" 34 | description = "The uncompromising code formatter." 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=3.8" 38 | 39 | [package.dependencies] 40 | click = ">=8.0.0" 41 | mypy-extensions = ">=0.4.3" 42 | packaging = ">=22.0" 43 | pathspec = ">=0.9.0" 44 | platformdirs = ">=2" 45 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 46 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 47 | 48 | [package.extras] 49 | colorama = ["colorama (>=0.4.3)"] 50 | d = ["aiohttp (>=3.7.4,!=3.9.0)", "aiohttp (>=3.7.4)"] 51 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 52 | uvloop = ["uvloop (>=0.15.2)"] 53 | 54 | [[package]] 55 | name = "click" 56 | version = "8.1.7" 57 | description = "Composable command line interface toolkit" 58 | category = "dev" 59 | optional = false 60 | python-versions = ">=3.7" 61 | 62 | [package.dependencies] 63 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 64 | 65 | [[package]] 66 | name = "colorama" 67 | version = "0.4.6" 68 | description = "Cross-platform colored terminal text." 69 | category = "dev" 70 | optional = false 71 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 72 | 73 | [[package]] 74 | name = "coverage" 75 | version = "7.4.0" 76 | description = "Code coverage measurement for Python" 77 | category = "dev" 78 | optional = false 79 | python-versions = ">=3.8" 80 | 81 | [package.dependencies] 82 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 83 | 84 | [package.extras] 85 | toml = ["tomli"] 86 | 87 | [[package]] 88 | name = "exceptiongroup" 89 | version = "1.2.0" 90 | description = "Backport of PEP 654 (exception groups)" 91 | category = "dev" 92 | optional = false 93 | python-versions = ">=3.7" 94 | 95 | [package.extras] 96 | test = ["pytest (>=6)"] 97 | 98 | [[package]] 99 | name = "factory-boy" 100 | version = "3.3.0" 101 | description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." 102 | category = "dev" 103 | optional = false 104 | python-versions = ">=3.7" 105 | 106 | [package.dependencies] 107 | Faker = ">=0.7.0" 108 | 109 | [package.extras] 110 | dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "sqlalchemy-utils", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"] 111 | doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] 112 | 113 | [[package]] 114 | name = "faker" 115 | version = "11.4.0" 116 | description = "Faker is a Python package that generates fake data for you." 117 | category = "dev" 118 | optional = false 119 | python-versions = ">=3.6" 120 | 121 | [package.dependencies] 122 | python-dateutil = ">=2.4" 123 | text-unidecode = "1.3" 124 | 125 | [[package]] 126 | name = "fastapi" 127 | version = "0.109.0" 128 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 129 | category = "dev" 130 | optional = false 131 | python-versions = ">=3.8" 132 | 133 | [package.dependencies] 134 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 135 | starlette = ">=0.35.0,<0.36.0" 136 | typing-extensions = ">=4.8.0" 137 | 138 | [package.extras] 139 | all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 140 | 141 | [[package]] 142 | name = "h11" 143 | version = "0.14.0" 144 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 145 | category = "dev" 146 | optional = false 147 | python-versions = ">=3.7" 148 | 149 | [[package]] 150 | name = "idna" 151 | version = "3.6" 152 | description = "Internationalized Domain Names in Applications (IDNA)" 153 | category = "dev" 154 | optional = false 155 | python-versions = ">=3.5" 156 | 157 | [[package]] 158 | name = "iniconfig" 159 | version = "2.0.0" 160 | description = "brain-dead simple config-ini parsing" 161 | category = "dev" 162 | optional = false 163 | python-versions = ">=3.7" 164 | 165 | [[package]] 166 | name = "isort" 167 | version = "5.13.2" 168 | description = "A Python utility / library to sort Python imports." 169 | category = "dev" 170 | optional = false 171 | python-versions = ">=3.8.0" 172 | 173 | [package.extras] 174 | colors = ["colorama (>=0.4.6)"] 175 | 176 | [[package]] 177 | name = "mypy" 178 | version = "1.8.0" 179 | description = "Optional static typing for Python" 180 | category = "dev" 181 | optional = false 182 | python-versions = ">=3.8" 183 | 184 | [package.dependencies] 185 | mypy-extensions = ">=1.0.0" 186 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 187 | typing-extensions = ">=4.1.0" 188 | 189 | [package.extras] 190 | dmypy = ["psutil (>=4.0)"] 191 | install-types = ["pip"] 192 | mypyc = ["setuptools (>=50)"] 193 | reports = ["lxml"] 194 | 195 | [[package]] 196 | name = "mypy-extensions" 197 | version = "1.0.0" 198 | description = "Type system extensions for programs checked with the mypy type checker." 199 | category = "dev" 200 | optional = false 201 | python-versions = ">=3.5" 202 | 203 | [[package]] 204 | name = "packaging" 205 | version = "23.2" 206 | description = "Core utilities for Python packages" 207 | category = "dev" 208 | optional = false 209 | python-versions = ">=3.7" 210 | 211 | [[package]] 212 | name = "pathspec" 213 | version = "0.12.1" 214 | description = "Utility library for gitignore style pattern matching of file paths." 215 | category = "dev" 216 | optional = false 217 | python-versions = ">=3.8" 218 | 219 | [[package]] 220 | name = "platformdirs" 221 | version = "4.1.0" 222 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 223 | category = "dev" 224 | optional = false 225 | python-versions = ">=3.8" 226 | 227 | [package.extras] 228 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.24)", "sphinx (>=7.1.1)"] 229 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest (>=7.4)"] 230 | 231 | [[package]] 232 | name = "pluggy" 233 | version = "1.3.0" 234 | description = "plugin and hook calling mechanisms for python" 235 | category = "dev" 236 | optional = false 237 | python-versions = ">=3.8" 238 | 239 | [package.extras] 240 | dev = ["pre-commit", "tox"] 241 | testing = ["pytest", "pytest-benchmark"] 242 | 243 | [[package]] 244 | name = "pydantic" 245 | version = "2.5.3" 246 | description = "Data validation using Python type hints" 247 | category = "main" 248 | optional = false 249 | python-versions = ">=3.7" 250 | 251 | [package.dependencies] 252 | annotated-types = ">=0.4.0" 253 | pydantic-core = "2.14.6" 254 | typing-extensions = ">=4.6.1" 255 | 256 | [package.extras] 257 | email = ["email-validator (>=2.0.0)"] 258 | 259 | [[package]] 260 | name = "pydantic-core" 261 | version = "2.14.6" 262 | description = "" 263 | category = "main" 264 | optional = false 265 | python-versions = ">=3.7" 266 | 267 | [package.dependencies] 268 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 269 | 270 | [[package]] 271 | name = "pytest" 272 | version = "7.4.4" 273 | description = "pytest: simple powerful testing with Python" 274 | category = "dev" 275 | optional = false 276 | python-versions = ">=3.7" 277 | 278 | [package.dependencies] 279 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 280 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 281 | iniconfig = "*" 282 | packaging = "*" 283 | pluggy = ">=0.12,<2.0" 284 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 285 | 286 | [package.extras] 287 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 288 | 289 | [[package]] 290 | name = "pytest-cov" 291 | version = "4.1.0" 292 | description = "Pytest plugin for measuring coverage." 293 | category = "dev" 294 | optional = false 295 | python-versions = ">=3.7" 296 | 297 | [package.dependencies] 298 | coverage = {version = ">=5.2.1", extras = ["toml"]} 299 | pytest = ">=4.6" 300 | 301 | [package.extras] 302 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 303 | 304 | [[package]] 305 | name = "python-dateutil" 306 | version = "2.8.2" 307 | description = "Extensions to the standard Python datetime module" 308 | category = "dev" 309 | optional = false 310 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 311 | 312 | [package.dependencies] 313 | six = ">=1.5" 314 | 315 | [[package]] 316 | name = "ruff" 317 | version = "0.1.14" 318 | description = "An extremely fast Python linter and code formatter, written in Rust." 319 | category = "dev" 320 | optional = false 321 | python-versions = ">=3.7" 322 | 323 | [[package]] 324 | name = "six" 325 | version = "1.16.0" 326 | description = "Python 2 and 3 compatibility utilities" 327 | category = "dev" 328 | optional = false 329 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 330 | 331 | [[package]] 332 | name = "sniffio" 333 | version = "1.3.0" 334 | description = "Sniff out which async library your code is running under" 335 | category = "dev" 336 | optional = false 337 | python-versions = ">=3.7" 338 | 339 | [[package]] 340 | name = "starlette" 341 | version = "0.35.1" 342 | description = "The little ASGI library that shines." 343 | category = "dev" 344 | optional = false 345 | python-versions = ">=3.8" 346 | 347 | [package.dependencies] 348 | anyio = ">=3.4.0,<5" 349 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 350 | 351 | [package.extras] 352 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 353 | 354 | [[package]] 355 | name = "text-unidecode" 356 | version = "1.3" 357 | description = "The most basic Text::Unidecode port" 358 | category = "dev" 359 | optional = false 360 | python-versions = "*" 361 | 362 | [[package]] 363 | name = "tomli" 364 | version = "2.0.1" 365 | description = "A lil' TOML parser" 366 | category = "dev" 367 | optional = false 368 | python-versions = ">=3.7" 369 | 370 | [[package]] 371 | name = "typing-extensions" 372 | version = "4.9.0" 373 | description = "Backported and Experimental Type Hints for Python 3.8+" 374 | category = "main" 375 | optional = false 376 | python-versions = ">=3.8" 377 | 378 | [[package]] 379 | name = "uvicorn" 380 | version = "0.18.3" 381 | description = "The lightning-fast ASGI server." 382 | category = "dev" 383 | optional = false 384 | python-versions = ">=3.7" 385 | 386 | [package.dependencies] 387 | click = ">=7.0" 388 | h11 = ">=0.8" 389 | 390 | [package.extras] 391 | standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] 392 | 393 | [metadata] 394 | lock-version = "1.1" 395 | python-versions = "^3.8" 396 | content-hash = "c9b5ce64682863a8122ada94810f2908ac2f5405394896a38719d0abb43b4e9e" 397 | 398 | [metadata.files] 399 | annotated-types = [] 400 | anyio = [] 401 | black = [] 402 | click = [] 403 | colorama = [] 404 | coverage = [] 405 | exceptiongroup = [] 406 | factory-boy = [] 407 | faker = [] 408 | fastapi = [] 409 | h11 = [] 410 | idna = [] 411 | iniconfig = [] 412 | isort = [] 413 | mypy = [] 414 | mypy-extensions = [] 415 | packaging = [] 416 | pathspec = [] 417 | platformdirs = [] 418 | pluggy = [] 419 | pydantic = [] 420 | pydantic-core = [] 421 | pytest = [] 422 | pytest-cov = [] 423 | python-dateutil = [] 424 | ruff = [] 425 | six = [] 426 | sniffio = [] 427 | starlette = [] 428 | text-unidecode = [] 429 | tomli = [] 430 | typing-extensions = [] 431 | uvicorn = [] 432 | -------------------------------------------------------------------------------- /rcs_pydantic/scheme.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional, Union 2 | 3 | from pydantic import BaseModel, ConfigDict, Field, validator 4 | from typing_extensions import Never 5 | 6 | from . import enums 7 | 8 | 9 | class RcsSMSBody(BaseModel): 10 | title: Optional[str] = Field(max_length=30, default=None) 11 | description: str = Field(max_length=100) 12 | media: Optional[str] = None 13 | 14 | 15 | class RcsLMSBody(BaseModel): 16 | title: Optional[str] = Field(max_length=30, default=None) 17 | description: str = Field(max_length=1300) 18 | media: Optional[str] = None 19 | 20 | 21 | class RcsMMSBody(BaseModel): 22 | title: Optional[str] = Field(max_length=30, default=None) 23 | description: str = Field(max_length=1300) 24 | media: Optional[str] = None 25 | 26 | 27 | class RcsCHATBody(BaseModel): 28 | title: Optional[str] = Field(max_length=30, default=None) 29 | description: str = Field(max_length=1300) 30 | media: Optional[str] = None 31 | 32 | 33 | class RcsSMSCarouselBody(BaseModel): 34 | title1: Optional[str] = Field(max_length=30, default=None) 35 | description1: str = Field(max_length=100) 36 | media1: Optional[str] = None 37 | title2: Optional[str] = Field(max_length=30, default=None) 38 | description2: Optional[str] = Field(max_length=100, default=None) 39 | media2: Optional[str] = None 40 | title3: Optional[str] = Field(max_length=30) 41 | description3: Optional[str] = Field(max_length=100, default=None) 42 | media3: Optional[str] = None 43 | title4: Optional[str] = Field(max_length=30) 44 | description4: Optional[str] = Field(max_length=100) 45 | media4: Optional[str] = None 46 | title5: Optional[str] = Field(max_length=30) 47 | description5: Optional[str] = Field(max_length=100) 48 | media5: Optional[str] = None 49 | title6: Optional[str] = Field(max_length=30) 50 | description6: Optional[str] = Field(max_length=100) 51 | media6: Optional[str] = None 52 | 53 | 54 | class RcsLMSCarouselBody(BaseModel): 55 | title1: Optional[str] = Field(max_length=30) 56 | description1: str = Field(max_length=1300) 57 | media1: Optional[str] = None 58 | title2: Optional[str] = Field(max_length=30, default=None) 59 | description2: Optional[str] = Field(max_length=1300, default=None) 60 | media2: Optional[str] = None 61 | title3: Optional[str] = Field(max_length=30, default=None) 62 | description3: Optional[str] = Field(max_length=1300, default=None) 63 | media3: Optional[str] = None 64 | title4: Optional[str] = Field(max_length=30, default=None) 65 | description4: Optional[str] = Field(max_length=1300, default=None) 66 | media4: Optional[str] = None 67 | title5: Optional[str] = Field(max_length=30, default=None) 68 | description5: Optional[str] = Field(max_length=1300, default=None) 69 | media5: Optional[str] = None 70 | title6: Optional[str] = Field(max_length=30, default=None) 71 | description6: Optional[str] = Field(max_length=1300, default=None) 72 | media6: Optional[str] = None 73 | 74 | 75 | class RcsMMSCarouselBody(BaseModel): 76 | title1: Optional[str] = Field(max_length=30) 77 | description1: str = Field(max_length=1300) 78 | media1: Optional[str] = None 79 | title2: Optional[str] = Field(max_length=30, default=None) 80 | description2: Optional[str] = Field(max_length=1300, default=None) 81 | media2: Optional[str] = None 82 | title3: Optional[str] = Field(max_length=30, default=None) 83 | description3: Optional[str] = Field(max_length=1300, default=None) 84 | media3: Optional[str] = None 85 | title4: Optional[str] = Field(max_length=30, default=None) 86 | description4: Optional[str] = Field(max_length=1300, default=None) 87 | media4: Optional[str] = None 88 | title5: Optional[str] = Field(max_length=30, default=None) 89 | description5: Optional[str] = Field(max_length=1300, default=None) 90 | media5: Optional[str] = None 91 | title6: Optional[str] = Field(max_length=30, default=None) 92 | description6: Optional[str] = Field(max_length=1300, default=None) 93 | media6: Optional[str] = None 94 | 95 | 96 | class RcsCHATCarouselBody(BaseModel): 97 | title1: Optional[str] = Field(max_length=30) 98 | description1: str = Field(max_length=1300) 99 | media1: Optional[str] = None 100 | title2: Optional[str] = Field(max_length=30, default=None) 101 | description2: Optional[str] = Field(max_length=1300, default=None) 102 | media2: Optional[str] = None 103 | title3: Optional[str] = Field(max_length=30, default=None) 104 | description3: Optional[str] = Field(max_length=1300, default=None) 105 | media3: Optional[str] = None 106 | title4: Optional[str] = Field(max_length=30, default=None) 107 | description4: Optional[str] = Field(max_length=1300, default=None) 108 | media4: Optional[str] = None 109 | title5: Optional[str] = Field(max_length=30, default=None) 110 | description5: Optional[str] = Field(max_length=1300, default=None) 111 | media5: Optional[str] = None 112 | title6: Optional[str] = Field(max_length=30, default=None) 113 | description6: Optional[str] = Field(max_length=1300, default=None) 114 | media6: Optional[str] = None 115 | 116 | 117 | class LocationInfo(BaseModel): 118 | query: Optional[str] = None 119 | longitude: Optional[float] = None 120 | latitude: Optional[float] = None 121 | label: Optional[str] = None 122 | 123 | 124 | class ShowLocationInfo(BaseModel): 125 | fallbackUrl: str = Field(max_length=1000) 126 | location: LocationInfo 127 | 128 | 129 | class PostParameterInfo(BaseModel): 130 | P_NAME: str 131 | P_MID: str 132 | 133 | 134 | class OpenUrlInfo(BaseModel): 135 | url: str 136 | isHalfView: Optional[str] = Field(default="false") 137 | postParameter: Optional[PostParameterInfo] = None 138 | 139 | 140 | class CreateCalendarEventInfo(BaseModel): 141 | title: str 142 | description: str 143 | startTime: str 144 | endTime: str 145 | 146 | 147 | class CopyToClipboardInfo(BaseModel): 148 | text: str 149 | 150 | 151 | class ComposeTextMessageInfo(BaseModel): 152 | phoneNumber: str 153 | text: str 154 | 155 | 156 | class DialPhoneNumberInfo(BaseModel): 157 | phoneNumber: str 158 | 159 | 160 | class UrlActionInfo(BaseModel): 161 | """ 162 | 단말기에 기본 웹 브라우저로 설정된 앱을 통해서, 웹페이지로 이동할 수있습니다 163 | """ 164 | 165 | openUrl: OpenUrlInfo 166 | 167 | 168 | class LocalBrowserActionInfo(BaseModel): 169 | """ 170 | 단말기의 메시지 앱 내부 브라우저를 통해 웹페이지로 이동할 수 있습니다 171 | """ 172 | 173 | openUrl: OpenUrlInfo 174 | 175 | 176 | class MapActionInfo(BaseModel): 177 | """ 178 | 미리 지정된 위치를 보여주거나 사용자의 현재 위치를 서버로 전송 할 수 있습니다. 179 | """ 180 | 181 | showLocation: Optional[ShowLocationInfo] = None 182 | requestLocationPush: Optional[Dict] = None 183 | 184 | 185 | class CalendarActionInfo(BaseModel): 186 | """ 187 | 사용자의 캘린더에 특정 일정을 등록 할 수 있습니다. 188 | """ 189 | 190 | createCalendarEvent: CreateCalendarEventInfo 191 | 192 | 193 | class ClipboardActionInfo(BaseModel): 194 | """ 195 | 특정 문구를 사용자 단말이 자동으로 복사 할 수 있게 합니다 196 | """ 197 | 198 | copyToClipboard: CopyToClipboardInfo 199 | 200 | 201 | class ComposeActionInfo(BaseModel): 202 | """ 203 | 다른 번호로 메시지를 보낼 수 있도록 대화방을 엽니다. 204 | """ 205 | 206 | composeTextMessage: ComposeTextMessageInfo 207 | 208 | 209 | class DialerActionInfo(BaseModel): 210 | """ 211 | 특정 전화번호로 전화를 걸 수 있습니다. 212 | """ 213 | 214 | dialPhoneNumber: DialPhoneNumberInfo 215 | 216 | 217 | class PostbackInfo(BaseModel): 218 | data: str = Field(max_length=2048) 219 | 220 | 221 | class ReplyActionInfo(BaseModel): 222 | """ 223 | 버튼을 눌러서 답장할수있습니다. 224 | """ 225 | 226 | displayText: str = Field(max_length=200) 227 | postback: PostbackInfo 228 | 229 | 230 | class ActionInfo(BaseModel): 231 | urlAction: Optional[UrlActionInfo] = None 232 | localBrowserAction: Optional[LocalBrowserActionInfo] = None 233 | mapAction: Optional[MapActionInfo] = None 234 | calendarAction: Optional[CalendarActionInfo] = None 235 | clipboardAction: Optional[ClipboardActionInfo] = None 236 | composeAction: Optional[ComposeActionInfo] = None 237 | dialerAction: Optional[DialerActionInfo] = None 238 | displayText: str = Field(max_length=200) 239 | postback: PostbackInfo 240 | 241 | 242 | class SuggestionInfo(BaseModel): 243 | action: Optional[ActionInfo] = None 244 | reply: Optional[ReplyActionInfo] = None 245 | 246 | 247 | class ButtonInfo(BaseModel): 248 | suggestions: List[SuggestionInfo] 249 | 250 | 251 | class CommonInfo(BaseModel): 252 | """ 253 | msgServiceType enum: [rcs, legacy] 254 | rcs : rcs 부달시 실패코드 회신 255 | rcs, legcy : rcs 부달시, xMS로fallback전송 256 | * legacy만 보낼 수는 없음 257 | """ 258 | 259 | msgId: str = Field(max_length=40) 260 | userContact: str = Field(max_length=40) 261 | scheduleType: Optional[enums.ScheduleTypeEnum] = None 262 | msgGroupId: Optional[str] = Field(max_length=20, default=None) 263 | msgServiceType: enums.MessageServiceTypeEnum 264 | 265 | 266 | class RcsInfo(BaseModel): 267 | model_config = ConfigDict(arbitrary_types_allowed=True) 268 | 269 | chatbotId: str = Field(max_length=40) 270 | agencyId: Optional[str] = Field(max_length=20, default=None) 271 | """ 272 | # agencyId 273 | 대행사 ID 274 | ktbizrcs (Default: KT 중계가 브랜드 대행사인 경우) 275 | Maximum : 20Byte 276 | * Agency ID는 "Rcs Biz Center - 브랜드 운영관리" 에서 기업의 브랜드가 대행사 권한을 부여한 대행사의 ID 277 | Agency ID가 "ktbizrcs" 가 아닌 경우 필수 입력 필요. 278 | """ 279 | agencyKey: str 280 | """ 281 | # agencyKey 282 | agencyId (대행사 ID) 와 매핑되는 대행사 Key 값 283 | [보안성 강화] agencyId - agencyKey 가 불일치 하는 경우, 284 | 통신사에서 실패 처리. 대행사 고객인 경우 필수값. * agencyKey 는 RBC 에서 발급 및 갱신 가능하며, 285 | 갱신시 기존(old) agencyKey 는 최대 24 시간 유효함 286 | """ 287 | brandKey: str 288 | """ 289 | # brandKey 290 | chatbotId (챗봇 ID) 소유 brandId (브랜드 ID) 와 매핑되는 브랜드 Key 값 291 | [보안성 강화] brandId - brandKey 가 불일치 하는 경우, 통신사에서 실패 처리. 292 | 대행사 고객인 경우 필수값. *brandKey 는 기존 RBC 에서 발급, 제공 중인 값 293 | """ 294 | 295 | messagebaseId: Union[enums.MessageEnum, enums.RCSMessageEnum, str] 296 | serviceType: enums.ServiceTypeEnum 297 | expiryOption: Optional[enums.ExpiryOptionEnum] = None 298 | """ 299 | # expiryOption 300 | 메시지 처리 옵션 enum: [1, 2] 301 | - 1: 최대3일 까지 결과 webhook 전송 (default) 302 | - 2: ‘설정시간’ 까지 결과 webhook 전송 303 | * ‘설정시간’ 은 통신사 정책에 따라 변경 가능, 304 | 현재 ‘설정시간’ 은 별도 안내 (ex. 10초~3분) 305 | 306 | * 양방향 메시지 발송 시에는2로 설정해서 발송해야 한다 307 | """ 308 | header: enums.HeaderEnum 309 | 310 | footer: Optional[str] = Field(max_length=20, pattern=r"^[\d-]*$", default=None) 311 | """ 312 | # footer 313 | 수신거부 전화번호 (숫자, - 만 가능, Max: 20 자리) 314 | - header가0인 경우, footer 입력 불가 315 | - header가1인 경우, footer 필수 316 | """ 317 | 318 | @validator("footer") 319 | def footer_validator(cls, v, values, **kwargs): 320 | if values["header"] == enums.HeaderEnum.NOT_ADVERTISE and v: 321 | raise ValueError("If header is 0 then footer can not be provided.") 322 | elif values["header"] == enums.HeaderEnum.ADVERTISE and not v: 323 | raise ValueError("If header is 1 then footer should be provided.") 324 | return v 325 | 326 | cdrId: Optional[str] = None 327 | """ 328 | # cdrId 329 | 청약 ID 기록시 기록한 ID로 과금 처리 된다. 330 | Ex) 331 | RCS ID: A, B, C청약을 한 기업이 토큰 인증을A로 받고, 332 | 메시지 발송은 cdrId에B로 기록하여 발송하면A로 과금 되던 것이B로 과금 수행한다. 333 | - 청약 ID만 기록 가능 334 | - 해당 기업의 청약인지 Value 체크 수행 335 | """ 336 | copyAllowed: Optional[bool] = True 337 | """ 338 | # copyAllowed 339 | 단말의 메시지 복사 기능 허용 여부. 340 | - true: 복사 허용 (default) 341 | - false: 복사 허용 하지 않음 342 | """ 343 | 344 | body: Union[ 345 | RcsSMSBody, 346 | RcsLMSBody, 347 | RcsMMSBody, 348 | RcsCHATBody, 349 | RcsSMSCarouselBody, 350 | RcsLMSCarouselBody, 351 | RcsMMSCarouselBody, 352 | RcsCHATCarouselBody, 353 | dict, 354 | ] 355 | """ 356 | # body 357 | 메시지베이스에서 치환할 파라미터의 정보를 담은 json object. 358 | Maximum : 10240Byte 359 | - RCSSMS, RCSLMS, RCSMMS인 경우 360 | - "title": max 30자 361 | - "description" : 362 | RCSSMS max 100자 363 | RCSLMS max 1300자 364 | RCSMMS 카드별 총합max 1300자. 365 | - "media" :"maapfile://{fileId}" 366 | - RCSTMPL인 경우 367 | (RCS Biz Center에 미리 등록된 메시지베이스의 변수부에 데이터 수록) 368 | 변수부 가변 값의 총 합이 90자 이내여야 한다. 369 | 370 | * [별첨1] 엑셀파일 참조(A2P+양방향 메시지양식으로 업데이트 필요) 371 | - RCSCHAT인 경우 372 | - "title": max 30자 373 | - "description" : 1300자 374 | - "media" :"maapfile://{fileId}" 375 | """ 376 | 377 | buttons: Optional[List[Union[ButtonInfo, Dict[Never, Never]]]] = None 378 | """ 379 | # buttons 380 | GSMA RCC.07의3.6.10.4의 ‘suggestions’ 규격에 준하여 버튼을 구성 381 | ㅇRCSSMS, RCSLMS, RCSMMS, RCSCHAT인 경우에만 사용. 382 | suggestions의 리스트이며 리스트의 각 항목은 carousel의 카드 순서에 맞도록 삽입된다. 383 | - RCSSMS 최대 버튼 수: 1개 384 | - RCSLMS 최대 버튼 수: 3개 385 | - RCSMMS card당 최대 버튼 수: 2개 386 | (carousel에서 버튼이 없는 카드는 {}로 표시, 예제 참조) 387 | ㅇRCSTMPL인 경우 388 | 버튼의 변수부를 등록 하여 사용하며, 해당 필드 허용하지 않음. 389 | """ 390 | 391 | chipList: Optional[List[SuggestionInfo]] = None 392 | """ 393 | # chipList 394 | GSMA RCC.07의3.6.10.4의 ‘suggestion’ 규격에 따라 chiplist를 구성(RCC.07의 기준 버전 확인 필요) 395 | ㅇRCSCHAT인 경우에만 사용. 396 | * Chiplist는 최대 11개까지 사용 가능하다 397 | """ 398 | 399 | replyId: Optional[str] = Field(max_length=40, default=None) 400 | """ 401 | # replyId 402 | 양기업의 양방향 momsg를 수신할 때 포함된 replyID를 그대로 넣어서 전송한다 403 | Maximum : 40Byte 404 | ㅇRCSCHAT인 경우에 반드시 포함되어야 하며, RCSCHAT이 아닌 경우에는 포함되면 발송이 실패된다 405 | *양방향서비스에서 양방향 대화의 세션을 관리하는 기준으로, 406 | 고객이 양방향 MO를 수행할 때마다 새롭게 할당되며, 유효시간은 24시간임. 407 | 유효시간이 만료된 replyId포함되어 발송되면 실패처리된다 408 | """ 409 | 410 | 411 | class LegacyInfo(BaseModel): 412 | """ 413 | (RCS부달시 RCS중계플랫폼이 SMS/LMS/MMS를 크로샷으로 fallback 전송. 414 | legacy만 보낼 수는 없으며, RcsInfo 영역이 반드시 존재 해야 함) 415 | """ 416 | 417 | serviceType: enums.LegacyServiceTypeEnum 418 | """ 419 | Fallback은SMS/LMS에 대해서만 제공하고, MMS,CHAT에 대해서는 제공하지 않는다 420 | """ 421 | callback: str = Field(max_length=20) 422 | subject: Optional[str] = Field(max_length=50, default=None) 423 | msg: str = Field(max_length=4000) 424 | """ 425 | SMS : 80Byte 426 | LMS : 4000Byte (한글 2000자 이내) 427 | MMS : 4000Byte (한글 2000자 이내 + 이미지, 동영상) 428 | """ 429 | contentCount: int = Field(ge=0, le=3) 430 | """ 431 | 미디어파일의 갯수 432 | - 0: SMS/LMS인 경우, 미디어파일 없는 경우 433 | - 1~3 : (TBD) MMS인 경우 434 | """ 435 | contentData: Optional[str] = Field(max_length=250, default=None) 436 | """ 437 | (TBD) MMS 에서 사용하는 이미지파일의 정보 438 | - 컨텐츠위치^컨텐츠타입^컨텐츠서브타입 439 | - 컨텐츠위치: File Upload시 수신받은 URL (‘http’ 로 시작하는 url로 사용) 440 | - 컨텐츠타입: IMAGE = 1, AUDIO = 2, VIDEO = 3 441 | - 컨텐츠서브타입: 442 | - [IMAGE] 0 = JPG 443 | - [AUDIO] 0 = AAC, 1 = MA3, MMF 444 | - [VIDEO] 0 = MP4+AAC 445 | - 여러 개인 경우, "|" 이용 446 | 예) http://10.217.59.209:5084/data/MEDIA/RCS/send/2021/08/09/test004.jpg^1^JPG 447 | Maximum : 250Byte 448 | """ 449 | prefix: Optional[str] = Field(max_length=10, default=None) 450 | """ 451 | RCS 전송성공 가능성 있는 fallback 메시지 전송시 삽입문구 452 | - 79998(전송성공불확실함), 55820(Revoked Message)... 등 453 | - SMS 는 msg(본문), LMS/MMS 는 subject(제목)내 문구 삽입 예) 재전송, RE: 454 | """ 455 | kisaOrigCode: Optional[int] = Field(ge=0, le=999999999, default=None) 456 | """ 457 | 최초 발신 사업자코드, 9 자리 숫자 형식 458 | [보안성 강화] 대행사 고객의 경우 (즉, KT 중계가 최초 발신 459 | 대행사가 아닌 경우) 최초 발신 사업자코드 전달 필수 460 | 예) 123456789 461 | """ 462 | 463 | 464 | class ErrorInfo(BaseModel): 465 | code: str 466 | message: str 467 | 468 | 469 | class LegacyErrorInfo(BaseModel): 470 | code: str 471 | message: str 472 | 473 | 474 | class ResponseErrorInfo(BaseModel): 475 | status: str 476 | error: ErrorInfo 477 | 478 | 479 | class ReasonInfo(BaseModel): 480 | code: str 481 | message: str 482 | 483 | 484 | class StatusInfo(BaseModel): 485 | """ 486 | 메시지 전송 결과 487 | """ 488 | 489 | rcsId: Optional[str] = Field(max_length=20, default=None) 490 | msgId: str = Field(max_length=40) 491 | userContact: Optional[str] = Field(max_length=40, default=None) 492 | status: enums.MessageStatusEnum 493 | serviceType: Union[enums.ServiceTypeEnum, enums.LegacyServiceTypeEnum, None] = None 494 | mnoInfo: Optional[enums.MnoInfoEnum] = None 495 | sentTime: Optional[str] = Field(pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$", default=None) 496 | reason: Optional[ReasonInfo] = None 497 | error: Optional[ErrorInfo] = None 498 | legacyError: Optional[LegacyErrorInfo] = None 499 | timestamp: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") 500 | autoReplyMsgId: Optional[str] = Field(max_length=40, default=None) 501 | postbackId: Optional[str] = Field(max_length=40, default=None) 502 | chatbotId: Optional[str] = Field(max_length=40, default=None) 503 | bill: Optional[enums.BillEnum] = None 504 | 505 | 506 | class QuerystatusInfo(BaseModel): 507 | queryId: Optional[str] = Field(max_length=40) 508 | moreToSend: Optional[int] = Field(ge=0, le=1) 509 | 510 | 511 | class ResponseInfo(BaseModel): 512 | status: str 513 | data: dict 514 | 515 | 516 | class TextMessageInfo(BaseModel): 517 | textMessage: str 518 | 519 | 520 | class FileMessage(BaseModel): 521 | fileName: str 522 | fileUrl: str 523 | fileMIMEType: str 524 | fileSize: int 525 | 526 | 527 | class FileMessageInfo(BaseModel): 528 | fileMessage: FileMessage 529 | 530 | 531 | class GeolocationPushMessage(BaseModel): 532 | label: Optional[str] 533 | timestamp: Optional[str] 534 | timeOffset: Optional[str] 535 | pos: Optional[str] 536 | radius: Optional[int] 537 | 538 | 539 | class UserLocationInfo(BaseModel): 540 | geolocationPushMessage: GeolocationPushMessage 541 | 542 | 543 | class MessageInfo(BaseModel): 544 | replyId: str = Field(max_length=40) 545 | eventType: enums.EventTypeEnum 546 | """ 547 | 양방향 MO메시지의 종류를 식별하기 위한 값으로, 총3개의 eventType이 사용된다. 각 eventType별로 548 | postbackId, PostbackData, displayText, messagebody 등의 설정여부가 달라진다 549 | - message 550 | 고객이 직접 메시지를 입력한 경우에 설정됨 551 | - response 552 | 고객이 단말 대화방에서 고정메뉴 또는 메시지의 reply 버튼 또는 chiplist의reply를 선택한 경우 설정 553 | - newuser 554 | 고객이 단말 대화방을 최초 진입 시 설정됨 555 | Maximum : 10Byte 556 | """ 557 | postbackId: Optional[str] = Field(max_length=40, default=None) 558 | postbackData: Optional[str] = Field(max_length=2048, default=None) 559 | displayText: Optional[str] = Field(max_length=200, default=None) 560 | messageBody: Union[TextMessageInfo, UserLocationInfo, FileMessageInfo, None] = None 561 | """ 562 | eventType이message인 경우에만 설정 563 | - 텍스트 메시지의 경우 샘플 564 | `{"textMessage": "hello world"}` 565 | - 파일 메시지의 경우 샘플. 566 | ``` 567 | { 568 | "fileMessage": { 569 | "fileName": "3686492106936898.jpeg", 570 | "fileUrl": "https://bd-media- hub.hermes.kt.com/data/chat/message/file/3686492106936898.jpeg", 571 | "fileMIMEType": "image/jpeg", 572 | "fileSize": 326130 573 | } 574 | } 575 | ``` 576 | - userLocation 메시지의 경우 샘플 577 | ``` 578 | { 579 | "geolocationPushMessage": { 580 | "label": "meeting location", 581 | "timestamp": "2021-08-10T09:19:44Z", 582 | "timeOffset": -540, 583 | "pos": "37.3587121 127.1151084", 584 | "radius": 0 585 | } 586 | } 587 | ``` 588 | Maximum : 10240Byte 589 | """ 590 | 591 | @validator("messageBody") 592 | def check_message_body(cls, v, values, **kwargs): 593 | if v: 594 | if values["eventType"] == enums.EventTypeEnum.MESSAGE: 595 | return v 596 | raise ValueError("messageBody is not allowed") 597 | 598 | userContact: str = Field(max_length=40) 599 | chatbotId: str = Field(max_length=40) 600 | timestamp: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") 601 | 602 | 603 | class SendInfo(BaseModel): 604 | common: CommonInfo 605 | rcs: RcsInfo 606 | legacy: Optional[LegacyInfo] = None 607 | 608 | 609 | class TokenInfo(BaseModel): 610 | rcsId: str 611 | rcsSecret: str 612 | grantType: str 613 | 614 | 615 | class FileRegistInfo(BaseModel): 616 | fileId: str 617 | usageType: enums.FileUsageTypeEnum 618 | usageService: enums.FileUsageServiceEnum 619 | mimeType: str 620 | file: bytes 621 | description: Optional[str] = None 622 | 623 | 624 | class FileInfo(BaseModel): 625 | fileId: Optional[str] = None 626 | usageType: enums.FileUsageTypeEnum 627 | usageService: enums.FileUsageServiceEnum 628 | mimeType: str 629 | status: enums.FileStatusEnum 630 | size: Optional[int] = None 631 | expiryDate: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") 632 | url: str 633 | -------------------------------------------------------------------------------- /rcs_pydantic/errors.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TupleEnum(Enum): 5 | @classmethod 6 | def _missing_(cls, value): 7 | for item in cls: 8 | if item.value[0] == value: 9 | return item 10 | 11 | @classmethod 12 | def has_value(cls, value): 13 | return value in [x[0] for x in cls._value2member_map_] 14 | 15 | 16 | class LegacyErrorCodeEnum(str, TupleEnum): 17 | pass 18 | 19 | 20 | class ErrorCodeEnum(TupleEnum): 21 | """ 22 | 삼성 MaaP Core Enum of error codes. 23 | """ 24 | 25 | # General errors 26 | MISSING_AUTHORIZATION_HEADER = ( 27 | 40001, 28 | "Valid access token in Authorization header is required for RESTful API calls.", 29 | ) 30 | MISSING_TOKEN = (40002, "Authorization header does not contain a token. Note that token type is required.") 31 | INVALID_TOKEN = (40003, "Token has been tampered.") 32 | TOKEN_HAS_EXPIRED = (40004, "Token Has Expired") 33 | MALFORMED_TOKEN_PAYLOAD = (40005, "Token payload has insufficient information.") 34 | INVALID_CLIENT_ID = (40006, "Token is not issued to the client.") 35 | INSUFFICIENT_SCOPE = (40007, "Token is not authorized to use the scope.") 36 | INTERNAL_SERVER_ERROR = (41000, "Internal Server Error") 37 | RCS_REQUEST_TIMEOUT = (41001, "RCS Request Timeout") 38 | REVOCATION_FAILED = (41002, "Revocation Failed") 39 | THROTTLED_BY_MESSAGE_RATE = (41003, "Throttled by message rate") 40 | RCS_SERVER_BUSY = (41004, "RCS Server Busy") 41 | RCS_SERVER_TEMPORARILY_UNAVAILABLE = (41005, "RCS Server Temporarily unavailable") 42 | SESSION_DOES_NOT_EXIST = (41006, "Session does not exist") 43 | EXPIRED_BEFORE_SESSION_ESTABLISHMENT = (41007, "RCS 세션 연결 전 만료되어 발송 실패 ") 44 | SESSION_ALREADY_EXPIRED = (41008, "Session already expired") 45 | DEVICE_NOT_SUPPORT_REVOCATION = (41009, "") 46 | IMDN_RECEIVED_EVEN_ALREADY_REVOKED = (41010, "IMDN received even already revoked") 47 | MESSAGE_WAS_ALREADY_REVOKED = (41011, "") 48 | CONNECTING_RCS_ALLOCATOR_FAILED = (41100, "Message Status on Response of Sending Message API") 49 | CONNECTING_RCS_ALLOCATOR_TIMEOUT = (41101, "Message Status on Response of Sending Message API") 50 | RCS_ALLOCATION_FAILED = (41102, "Message Status on Response of Sending Message API") 51 | RCS_ALLOCATION_TIMEOUT = (41103, "Message Status on Response of Sending Message API") 52 | CONNECTING_RCS_FAILED = (41104, "Message Status on Response of Sending Message API") 53 | CONNECTING_RCS_TIMEOUT = (41105, "Message Status on Response of Sending Message API") 54 | SENDING_MESSAGE_TO_RCS_FAILED = (41106, "Message Status on Response of Sending Message API") 55 | SENDING_MESSAGE_TO_RCS_TIMEOUT = (41107, "Message Status on Response of Sending Message API") 56 | RCS_HANDLE_REQUEST_FAILED = (41108, "Message Status Webhook ") 57 | RCS_INTERNAL_SERVER_ERROR = (41109, "Message Status Webhook ") 58 | RCS_USER_NOT_FOUND = (41110, "Message Status Webhook ") 59 | PROCESS_REVOCATION_REQUEST_FAILED = (41117, "Message Status Webhook ") 60 | USER_FOR_MESSAGE_RECEIVING_NOT_FOUND = (41200, "") 61 | MESSAGE_NOT_ACCEPTABLE = ( 62 | 41201, 63 | "Message Session형성과정 중 단말의 SDP에 message송신에 필요한 feature tag가 없는 경우 발생", 64 | ) # noqa: E501 65 | USER_IS_NOT_CAPABLE_FOR_TEXT = (41210, "Bot이 Text message를 보낼 때 user의 capability에 chat이 없는 경우 발생.") 66 | USER_IS_NOT_CAPABLE_FOR_FT = (41211, "Bot이 File message를 보낼 때 user의 capability에 fthttp가 없는 경우 발생") 67 | USER_IS_NOT_CAPABLE_FOR_RICHCARD = ( 68 | 41212, 69 | "Bot이 Richcard message를 보낼 때 user의 capability에 bot 또는 chatbot.sa가 없는 경우 발생", 70 | ) 71 | USER_IS_NOT_CAPABLE_FOR_XBOTMESSAGE_1_0 = ( 72 | 41220, 73 | "clipboardAction, messageHeader, messageFooter, openrichcard, geolocationPushMessage, copyAllowed", 74 | ) 75 | USER_IS_NOT_CAPABLE_FOR_XBOTMESSAGE_1_1 = ( 76 | 41221, 77 | "clipboardAction, localBrowserAction, messageHeader, messageFooter, openrichcard, geolocationPushMessage," 78 | "copyAllowed Bot이 v1.1에 해당하는 Extended message를 보낼 때 user의 capability에 bot, chatbot.sa, xbotmessage " 79 | "1.1이 없는 경우 발생. 이 때 xbotmessage는 상위 version이 하위 version을 포함한다. (1.2 version이 있다면 1.1에 해당하는 " # noqa: E501 80 | "capability를 가진 것으로 간주함)", 81 | ) 82 | USER_IS_NOT_CAPABLE_FOR_XBOTMESSAGE_1_2 = ( 83 | 41222, 84 | "clipboardAction, localBrowserAction, messageHeader, messageFooter, openrichcard, geolocationPushMessage," 85 | " copyAllowed, shareAction, streamingPlay", 86 | ) 87 | USER_IS_NOT_CAPABLE_FOR_OPENRICHARD_1_0 = ( 88 | 41230, 89 | "Bot이 v1.0에 해당하는 Openrichcard를 보낼 때 user의 capability에 bot, chatbot.sa, xbotmessage 1.0이 없는 경우 발생", # noqa: E501 90 | ) 91 | USER_IS_NOT_CAPABLE_FOR_OPENRICHARD_1_1 = ( 92 | 41231, 93 | "v1.1에 해당하는 Openrichcard가 현재 정의 및 사용되지 않으므로, 실제 발생하지 않는 code", 94 | ) # noqa: E501 95 | USER_IS_NOT_CAPABLE_FOR_OPENRICHARD_1_2 = ( 96 | 41232, 97 | "v1.2에 해당하는 Openrichcard가 현재 정의 및 사용되지 않으므로, 실제 발생하지 않는 code", 98 | ) # noqa: E501 99 | USER_IS_NOT_CAPABLE_FOR_GEOLOCATION_PUSH_REQUEST = (41240, "User is not capable for GEOLOCATION PUSH REQUEST") 100 | FAILED_TO_GET_MESSAGE_CONTENT_TYPE = (41250, "Failed to get message content type") 101 | FILE_DOWNLOAD_FAILED = (41300, "") 102 | INVALID_STATE = (42001, "Bot is in an invalid state to call the API.") 103 | INVALID_MESSAGE = (42002, "Message is missing required fields or has malformed format.") 104 | INVALID_DATE_TIME_FORMAT = (42003, "Date and time format does not comply with ISO 8601.") 105 | MISSING_CONTACT = (42004, "Missing recipient information.") 106 | INVALID_CONTACT = (42005, "Incorrect contact format (e.g., phone number is not start with '+')") 107 | EMULATOR_ACCESS_ONLY = (42006, "Bot is in a state which allows access via Emulator only.") 108 | CONTACT_NOT_IN_WHITELIST = (42007, "Bot is accessible only to authorized users via MaaP client.") 109 | MISSING_MESSAGE_CONTENT = (42008, "Token Has Expired") 110 | INVALID_MESSAGE_CONTENT = ( 111 | 42009, 112 | "Message is not any of textMessage, fileMessage, audioMessage, geolocationPushMessage, richcardMessage," 113 | " or isTyping.", 114 | ) 115 | AMBIGUOUS_MESSAGE = ( 116 | 42010, 117 | "One and only one of textMessage, fileMessage, audioMessage, geolocationPushMessage, richcardMessage, " 118 | "or isTyping should be provided if sending a message to the user.", 119 | ) 120 | INVALID_MESSAGE_STATUS = (42011, "Invalid MessageStatus") 121 | INVALID_ISTYPING_STATUS = (42012, "Invalid IsTyping Status") 122 | INVALID_TRAFFIC_TYPE = (42013, "Invalid TrafficType") 123 | INVALID_SUGGESTED_CHIPLIST_ASSOCIATION = ( 124 | 42014, 125 | "suggestedChipList can be used together with one and only one of textMessage, fileMessage, audioMessage, " 126 | "geolocationPushMessage, or richcardMessage.", 127 | ) 128 | EMPTY_TEXT_MESSAGE = (42015, "Empty TextMessage") 129 | MISSING_RICHCARD = ( 130 | 42031, 131 | "richcardMessage should contain a message, which should contain either generalPurposeCard or" 132 | " generalPurposeCardCarousel.", 133 | ) 134 | AMBIGUOUS_RICHCARD = (42032, "A message should contain either richcard or richcard carousel.") 135 | TOO_MANY_RICHCARDS = (42033, "Richcard carousel exceeds the maximum allowed richcards.") 136 | MISSING_RICHCARD_LAYOUT = (42034, "generalPurposeCard or generalPurposeCardCarousel should contain layout.") 137 | MISSING_RICHCARD_CONTENT = (42035, "generalPurposeCard or generalPurposeCardCarousel should contain content.") 138 | INVALID_CARDORIENTATION = (42036, "cardOrientation should be either HORIZONTAL or VERTICAL.") 139 | MISSING_IMAGE_ALIGNMENT = (42037, "Richcard HORIZONAL orientation should contain image alignment.") 140 | INVALID_IMAGE_ALIGNMENT = (42038, "imageAlignment should be either LEFT or RIGHT.") 141 | REDUNDANT_IMAGE_ALIGNMENT = (42039, "Richcard VERTICAL orientation should not contain image alignment.") 142 | INVALID_RICHCARD_CAROUSEL_CARDWIDTH = ( 143 | 42040, 144 | "Richcard carousel cardWidth should be either SMALL_WIDTH or MEDIUM_WIDTH.", 145 | ) 146 | MISMATCHED_MEDIA_HEIGHT = (42041, "Media height cannot be TALL_HEIGHT for SMALL_WIDTH carousel.") 147 | INVALID_RICHCARD_CONTENT = ( 148 | 42042, 149 | "generalPurposeCard or generalPurposeCardCarousel content should have at least one of media, title, " 150 | "or description.", 151 | ) 152 | INVALID_SUGGESTIONS = (42043, "Suggestion list should contain at least one suggested reply or action.") 153 | TOO_MANY_SUGGESTIONS_IN_CHIPLIST_MAX_11 = (42044, "Suggestion list exceeds the maximum allowed items.") 154 | INVALID_SUGGESTION = (42045, "Suggestion should be either reply or action.") 155 | AMBIGUOUS_SUGGESTION = (42046, "Suggestion should be one and only one reply or action.") 156 | AMBIGUOUS_SUGGESTED_ACTION = (42047, "Suggested action should be one and only one type of action.") 157 | TOO_MANY_SUGGESTIONS_IN_RICHCARD_MAX_4 = (42048, "Richcard suggestion list exceeds the maximun allowed items (4).") 158 | TOO_MANY_SUGGESTION_DATA_MAX_2048 = (42049, "Suggestion data exceeds the maxmun data size (2048).") 159 | INVALID_ACTION = (42050, "Either latitude and longitude or query are required in location.") 160 | INVALID_LOCATION = (42051, "Either latitude and longitude or query are required in location.") 161 | AMBIGUOUS_LOCATION = (42052, "Location should contain either query or latitude and longitude.") 162 | INVALID_MAPACTION = (42053, "One of showLocation or requestLocationPush is required in mapAction.") 163 | AMBIGUOUS_MAPACTION = (42054, "mapAction should contain either showLocation or requestLocationPush.") 164 | INVALID_DIALERACTION = ( 165 | 42055, 166 | "One of dialPhoneNumber, dialEnrichedCall, or dialVideoCall is required in dialerAction.", 167 | ) 168 | AMBIGUOUS_DIALERACTION = (42056, "dialerAction should contain only one type of dialer action.") 169 | INVALID_COMPOSEACTION = ( 170 | 42057, 171 | "Either composeTextMessage or composeRecordingMessage is required in composeAction.", 172 | ) 173 | AMBIGUOUS_COMPOSEACTION = (42058, "composeAction should contain only one type of compose action.") 174 | INVALID_SETTINGSACTION = ( 175 | 42059, 176 | "Either enableDisplayedNotifications or disableAnonymization is required in settingsAction.", 177 | ) 178 | AMBIGUOUS_SETTINGSACTION = (42060, "settingsAction should contain only one type of settings action.") 179 | INVALID_CLIPBOARDACTION = (42061, "ClipboardAciton should contain copy text.") 180 | MISSING_LOCALBROWSERACTION = (42062, "LocalBrowserAction should contain url.") 181 | INVALID_SHAREACTION = (42063, "ShareAction should contain url.") 182 | AMBIGUOUS_SHAREACTION = (42064, "Sharetext of ShareAction should contain text.") 183 | INVALID_THUMBNAIL = ( 184 | 42100, 185 | "thumbnailFileSize and thumbnailContentType should be provided together with thumbnailUrl.", 186 | ) 187 | MISSING_FILEURL = (42101, "Missing FileUrl") 188 | MISSING_AUDIO_FILEURL = (42102, "Missing AudioFile Url") 189 | MISSING_POS = (42103, "Missing Pos") 190 | MISSING_MEDIA_INFORMATION = (42104, "Missing Media Information") 191 | MISSING_MEDIA_CONTENT_TYPE = (42105, "Missing Media Information") 192 | MISSING_MEDIA_FILESIZE = (42106, "Missing Media Information") 193 | MISSING_MEDIA_HEIGHT = (42107, "Missing Media Information") 194 | MISSING_POSTBACK_DATA_INFORMATION = (42108, "Missing postback data information") 195 | INVALID_LOCATION_LABEL = (42201, "geolocationPushMessage should not contain label length > 200 Characters.") 196 | INVALID_MEDIA_CONTENT_DESCRIPTION = ( 197 | 42202, 198 | "Content Description of media is not in limit <1 Character or >200 Character.", 199 | ) 200 | INVALID_MEDIA_TITLE = (42203, "Media title is not in limit <1 Character or >200 Character.") 201 | INVALID_MEDIA_DESCRIPTION = (42204, "Media card description is not in limit <1 Character or >2000 Character.") 202 | TOO_MANY_CONTENTS_IN_RICHARD_CAROUSEL = (42205, "Richcard carousel list exceeds the maximum allowed items (10).") 203 | INVALID_MEDIA_FILE_SIZE = (42206, "Minimum Value for file size is 0.") 204 | INVALID_EXPIRY_TIME_FORMAT = (42207, "Invalid expiry time") 205 | INVALID_EXPIRY_TIME = (42208, "Invalid expiry time") 206 | AMBIGUOUS_OPENRICHCARD = ( 207 | 42301, 208 | "OpenrichcardMessage should contain a message, which should contain either generalPurposeCard or " 209 | "generalPurposeCardCarousel.", 210 | ) 211 | MISSING_OPENRICHCARD = (42302, "A OpenrichcardMessage should contain open_rich_card.") 212 | MISSING_OPENRICHCARD_LAYOUT_WIDGET = (42303, "Openrichcard should contain widget.") 213 | MISSING_OPENRICHCARD_VIEW_CONTENT = (42304, "View layout of Openrichcard do not have mandatory parameters.") 214 | MISSING_OPENRICHCARD_LINEARLAYOUT_CONTENT = (42305, "Parameters in LinearLayout have invalid value.") 215 | MISSING_OPENRICHCARD_TEXTVIEW_CONTENT = (42306, "TextView layout of Openrichcard do not have mandatory parameters.") 216 | INVALID_CONTENTS_IN_OPENRICHCARD_TEXTVIEW = (42307, "Parameters in TextView have invalid value.") 217 | INVALID_TEXT_LENGTH_IN_OPENRICHCARD_TEXTVIEW = (42308, "text is not limit in [0 < text < 2000].") 218 | MISSING_OPENRICHCARD_IMAGEVIEW_CONTENT = ( 219 | 42309, 220 | "ImageView layout of Openrichcard do not have mandatory parameters.", 221 | ) 222 | TOO_SMALL_MEDIA_IN_OPENRICHCARD_IMAGEVIEW = (42310, "MediaFileSize is under 0.") 223 | INVALID_OPENRICHCARD_IMAGEVIEW_SCALETYPE = ( 224 | 42311, 225 | "ScaleType value of ImageView should be center or centerCrop or centerInside or fitCenter or fitEnd or " 226 | "fitStart or fitXY or matrix.", 227 | ) 228 | MISSING_OPENRICHCARD_WIDTH_OR_HEIGHT = (42312, "width or height is not in Openrichcard.") 229 | INVALID_OPENRICHCARD_WIDTH_OR_HEIGHT = (42313, "width or height in Openrichcard has invalid value.") 230 | INVALID_OPENRICHCARD_COMMON_CONTENTS = (42314, "Common Contents in Openrichcard has invalid value.") 231 | TOO_MANY_CHILD_IN_OPEN_RICH_CARD = (42315, "") 232 | INVALID_FILE_TYPE = (42401, "Invalid file type") 233 | DOWNLOAD_FAILURE = (42402, "Download failure") 234 | MISSING_CONTACT_1 = (42501, "Missing contact") 235 | MISSING_CONTENT_2 = (42502, "Missing content") 236 | MISSING_TITLE = (42503, "Missing title") 237 | MISSING_DESCRIPTION = (42504, "Missing description") 238 | MISSING_IMAGE_URL = (42505, "Missing image url") 239 | MISSING_IMAGE_TYPE = (42506, "Missing image type") 240 | MISSING_BUTTON_LINK = (42507, "Missing button link") 241 | MISSING_BUTTON_TEXT = (42508, "Missing button text") 242 | INVALID_TITLE = (42509, "Invalid title") 243 | INVALID_DESCRIPTION = (42510, "Invalid description") 244 | INVALID_IMAGE_URL_ADDRESS = (42511, "Invalid image url address") 245 | INVALID_BUTTON_URL_ADDRESS = (42512, "Invalid button url address") 246 | INVALID_BUTTON_TEXT = (42513, "Invalid button text") 247 | DUPLICATED_MESSAGE_ID = (42514, "Duplicated message ID") 248 | TOO_MANY_REQUEST = (42601, "Too Many Request") 249 | 250 | 251 | class MaaPErrorCodeEnum(TupleEnum): 252 | """ 253 | 이동통신사 MaaP Core Enum of error codes. 254 | """ 255 | 256 | MISSING_AUTHORIZATION_HEADER = (50001, "Authorization 헤더 파라미터 누락") 257 | MISSING_TOKEN = (50002, "Authorization 헤더 값 누락") 258 | INVAILD_TOKEN = (50003, "토큰이 일치하지 않습니다.") 259 | TOKEN_HAS_EXPIRED = (50004, "토큰이 만료되었습니다.") 260 | AUTHORIZATION_ERROR = (50005, "인증 토큰 에러") 261 | INVALID_CLIENT_ID = (50006, "요청된 계정 정보를 찾을 수 없습니다(BP ID)") 262 | INVALID_SENDER_ID = (50007, "요청된 중계사 전송 계정을 찾을 수 없습니다(RCS ID)") 263 | INVALID_PASSWORD = (50008, "잘못된 패스워드") 264 | NON_ALLOWED_IP = (50009, "접근 허용된 IP가 아닙니다") 265 | INVALID_STATE = (50100, "메시지 전송을 할 수 없는 상태입니다. (서버의 요청 거부)") 266 | MESSAGE_TPS_EXCEEDED = (50201, "RCS 메시지 TPS가 초과되었습니다.") 267 | MESSAGE_QUOTA_EXCEEDED = (50202, "RCS 메시지 Quota가 초과되었습니다.") 268 | SYSTEM_ERROR = (59001, "시스템 에러") 269 | IO_ERROR_IO = (59002, "에러 발생") 270 | DUPLICATION_ERROR = (51003, "중복 Key 오류") 271 | PARAMETER_ERROR = (51004, "요청 파라미터 형식 오류") 272 | JSON_PARSING_ERROR = (51005, "요청 Body JSON 파싱 에러") 273 | DATA_NOT_FOUND = (51006, "데이터를 찾을 수 없음") 274 | DUPLICATED_AUTO_REPLY_MSG_ID = (51007, "이미 사용 중인 자동응답 메시지 ID입니다.") 275 | DUPLICATED_POSTBACK_ID = (51008, "이미 사용 중인 Postback ID입니다.") 276 | INVALID_PHONE_NUMBER_FORMAT = (52001, "전화번호 형식이 일치하지 않습니다") 277 | INVALID_MESSAGE_STATUS = (52002, "요청을 처리할 수 없는 상태입니다.") 278 | BOT_ALEADY_EXISTS = (52003, "이미 사용 중인 챗봇 ID입니다.") 279 | BOT_CREATION_FAILED = (52004, "챗봇을 생성할 수 없습니다.") 280 | BOT_UPDATE_FAILED = (52005, "챗봇 정보를 변경할 수 없습니다.") 281 | BRAND_DELETE_FAILED = (52006, "챗봇이 있는 브랜드는 삭제 할수 없습니다.") 282 | INVALID_CHATBOT_SERVICE_TYPE = (52007, "챗봇 Type은 a2p, chatbot 로 설정해야 함") 283 | MISMATCHED_CHATBOT_ID = (52008, "요청 URL Parameter의 챗봇 Id와 Body Parameter 불일치") 284 | PERSISTENT_MENU_PERMISSION_ERROR = (52009, "Persistent Menu 등록이 허용되지 않습니다.") 285 | INVALID_PERSISTENT_MENU_DATA = (52010, "Persistent menu JSON 데이터 오류") 286 | MESSAGE_TRANSMISSION_TIME_EXCEEDING = (52016, "실시간 메시지 인입 후 10초안에 삼성으로 전달되지 못함") 287 | MESSAGEBASE_ID_STOPPED_TEMPORARILY = (52023, "메시지 베이스의 상태가 'pause'인 메시지 베이스 메시지로 전문 구성하여 전송 시도 시") # noqa: E501 288 | INVALID_WEBHOOK_REQUEST_PARAMETER = (52101, "잘못된 Webhook 중계사 요청 파라미터 입니다.") 289 | WEBHOOK_HOST_CONNECT_ERROR = (52102, "Webhook 중계 시스템 연결 오류") 290 | WEBHOOK_HOST_SERVER_REQUEST_FAILURE = (52103, "중계사 Webhook 전송 요청을 실패 했습니다.") 291 | WEBHOOK_RESPONSE_RECEIVE_FAILURE = (52104, "중계사 Webhook 처리 응답 수신 오류가 발생 했습니다.") 292 | WEBHOOK_MESSAGE_NON_RECEIVE_FAILURE = (52105, "Webhook 메시지 미 수신 오류가 발생 했습니다.") 293 | WEBHOOK_MESSAGE_PROCESS_FAILURE = (52106, "Webhook 메시지 처리 오류가 발생 했습니다.") 294 | INVALID_AUTO_REPLY_MESSAGE_ID = (52201, "자동응답 메시지 ID가 존재하지 않습니다.") 295 | AUTO_REPLY_MESSAGE_CONTENTS_ERROR = (52202, "자동응답 메시지 내용 중 누락된 필수 항목이 있습니다.") 296 | INVALID_FILE_TYPE = (53001, "요청을 처리할 수 없는 파일 유형입니다.") 297 | FILE_ATTRIBUTE_ERROR = (53002, "파일 속성 오류") 298 | FILE_ID_FORMAT_ERROR = (53003, "fileID가 없거나 ID형식에 맞지 않음") 299 | FILE_UPLOAD_ERROR = (53004, "File 저장 오류") 300 | INVALID_MULTI_PART_REQUEST = (53005, "Multipart 데이터 전송 오류") 301 | ATTACHED_FILE_SIZE_ERROR = (53006, "업로드 파일 크기 초과") 302 | INVAILD_CONTACT_NUMBER_USER = (54001, "자사 고객이 아닙니다.") 303 | NO_RCS_CAPABILITY = (54002, "자사 고객이지만, RCS메시지를 수신할 수 있는 가입자가 아닙니다.") 304 | UNABLE_SENDING_TO_RECIPIENT = (54003, "단말기기로 RCS 메시지를 전송할 수 없습니다.") 305 | MAA_P_INTERNAL_ERROR = (54004, "MaaP 시스템 혹은 RCS 프로토콜 상의 이슈로 발송 실패되었음 (삼성 에러 40001 ~ 41100, 42601)") # noqa: E501 306 | CORP_CONTENT_ERROR = (55001, "기업 정보 내용이 누락된 필수항목이 있습니다.") 307 | INVALID_PROPERTY = (55002, "필수 파라미터 검증 오류") 308 | AGENCY_CONTENT_ERROR = (55101, "대행사 정보 내용이 누락된 필수 항목이 있습니다.") 309 | INVALID_AGENCY_ID = (55102, "AgencyID가 존재하지 않습니다.") 310 | AGENCY_ID_PERMISSION_ERROR = (55103, "BrandID에 대행 권한이 없는 AgencyID") 311 | CONTRACT_CONTENT_ERROR = (55104, "계약 정보 내용이 부정확하거나 누락된 필수 항목이 있습니다.") 312 | BRAND_CONTENT_ERROR = (55201, "브랜드 정보 내용이 누락된 필수항목이 있습니다.") 313 | BRAND_NAME_ERROR = (55202, "브랜드 명이 누락되어 있습니다.") 314 | BRAND_PROFILE_IMAGE_ERROR = (55203, "브랜드 프로필 이미지가 누락되어 있습니다.") 315 | BRAND_CS_NUMBER_ERROR = (55204, "브랜드 CS번호가 누락되어 있습니다.") 316 | BRAND_MENU_ERROR = (55205, "브랜드 메뉴 최대 개수를 초과하였거나 부정확합니다. ") 317 | BRAND_CATEGORY_ERROR = (55206, "브랜드 카테고리 설정이 잘못되어 있습니다. ") 318 | BRAND_HOMEPAGE_ERROR = (55207, "브랜드 홈페이지 설정이 잘못되어 있습니다.") 319 | BRAND_EMAIL_ERROR = (55208, "브랜드 이메일 설정이 잘못되어 있습니다.") 320 | BRAND_ADDRESS_ERROR = (55209, "브랜드 주소가 잘못되어 있습니다.") 321 | INVALID_BRAND_ID = (55210, "브랜드ID가 존재하지 않음") 322 | BOT_CONTENT_ERROR = (55301, "챗봇 정보 내용이 부정확하거나 누락된 필수항목이 있습니다.") 323 | INVALID_BOT_ID = (55302, "BotID(발신번호)가 전화번호 형식에 맞지 않음") 324 | BOT_ID_PERMISSION_ERROR = (55303, "BrandID에 존재하지 않는 BotID") 325 | MESSAGEBASE_CONTENT_ERROR = (55501, "메시지베이스 내용이 부정확하거나 누락된 필수항목이 있습니다.") 326 | INVALID_MESSAGEBASE_ID = (55502, "MessagebaseID가 존재하지 않음") 327 | MESSAGEBASE_ID_PERMISSION_ERROR = (55503, "BrandID에 존재하지 않는 MessagebaseID입니다.") 328 | INVALID_FORMATSTRING = (55504, "messagebase의 formatstring 누락된 필수 항목이 있습니다.") 329 | INVALID_MESSAGEBASE_POLICY_INFO = (55505, "messagebase의 policy Info가 부정확하거나 누락된 필수 항목이 있습니다.") 330 | INVALID_MESSAGEBASE_PARAM = (55506, "messagebase의 param 부정확하거나 누락된 필수 항목이 있습니다.") 331 | INVALID_MESSAGEBASE_ATTRIBUTE = (55507, "messagebase의 attribute 부정확하거나 누락된 필수 항목이 있습니다.") 332 | INVALID_MESSAGEBASE_TYPE = (55508, "messagebase의 type 부정확하거나 누락된 필수 항목이 있습니다.") 333 | MISMATCHING_PRODUCT_TYPE = (55509, "messagebaseID의 product type과 일치하지 않음") 334 | MESSAGEBASE_FORM_CONTENT_ERROR = (55601, "MessagebaseForm 내용이 부정확하거나 누락된 필수항목이 있습니다.") 335 | INVALID_MESSAGEBASEFORM_ID = (55602, "messagebaseformID가 존재하지 않습니다.") 336 | INVALID_MESSAGE_BASE_PRODUCT_CODE = (55603, "messaegBase의 상품코드 에러") 337 | PROHIBITED_TEXT_CONTENT = (55701, "(광고) 를 사용할 수 없음") 338 | ACTION_BUTTON_PERMISSION_ERROR = ( 339 | 55702, 340 | "Action button이 허용되지 않는 messagebaseID에서 Action button을 사용하였음", 341 | ) # noqa: E501 342 | PROHIBITED_HEADER_VALUE = (55703, "허용되지 않은 header 값 사용") 343 | PROHIBITED_FOOTER_FIELD = (55704, "header 값과 일치 하지 않은 footer 사용 (ex. header가 0 인데, footer 가 있음)") 344 | MISSING_FOOTER_CONTENT = (55705, "footer값이 누락되어 있습니다 (ex. header가 1 인데, footer 가 없음)") 345 | FOOTER_CONTENT_SYNTAX_ERROR = (55706, "footer validation 오류 (ex. 숫자, 하이픈만 가능. 20자리)") 346 | CONTENT_PATTERN_ERROR = (55707, "등록한 패턴과 일치 하지 않음") 347 | EXCEEDED_MAX_CHARACTER_OF_TITLE = (55708, "title 최대글자수를 초과했습니다.") 348 | EXCEEDED_MAX_CHARACTER_OF_DESCRIPTION = (55709, "description 최대글자수를 초과했습니다.") 349 | EXCEEDED_MAX_NUMBER_OF_BUTTONS = (55710, "최대 버튼수를 초과했습니다.") 350 | MISMATCHING_NUMBER_OF_CAROUSEL_CARD = (55711, "messagebaseID의 number of card 와 입력이 일치하지 않음") 351 | EXCEEDED_MAX_SIZE_OF_MEDIA = (55712, "최대 미디어 용량을 초과했습니다.") 352 | EXCEEDED_REPLY_ID_USAGE_COUNT = (55713, "Reply ID의 사용횟수 초과했습니다.") 353 | EXPIRED_REPLY_ID = (55714, "Reply ID의 유효시간이 만료되었습니다.") 354 | NOT_FOUND_REPLY_ID = (55715, "Reply ID가 존재하지 않음") 355 | GW_VENDOR_CONTENT_ERROR = (55801, "중계사 정보가 부정확하거나 누락된 필수 항목이 있습니다.") 356 | INVALID_MESSAGE = (55802, "메시지 형식이 부정확하거나 누락된 필수항목이 있습니다.") 357 | MESSAGE_SYNTAX_ERROR = (55803, "메시지 기술방법이 잘못되었습니다.") 358 | MISSING_MESSAGE_CONTENT = (55804, "메시지 내용이 누락되었거나 부정확합니다.") 359 | INVALID_MESSAGE_CONTENT = (55805, "요청을 처리할 수 없는 메시지 유형입니다.") 360 | DUPLICATED_MESSAGE_ID = (55806, "같은 메시지 ID로 두번 이상 메시지 발송이 요청됨") 361 | INVALID_CHATBOT_PERMISSION = (55807, "챗봇 권한 오류") 362 | INVALID_CHATBOT_STATUS = (55808, "발신 가능한 챗봇 상태가 아님") 363 | INVALID_AGENCY_PERMISSION = (55809, "대행사 권한 오류") 364 | INVALID_EXPIRY_FIELD = (55810, "메시지 유효기간 입력값 오류") 365 | EXCEEDED_MAX_CHARACTER_OF_PARAM = (55811, "메시지베이스 파라미터의 길이가 한계값 이상") 366 | BUTTONS_NOT_ALLOWED = (55812, "버튼 필드를 받을 수 없는 메시지베이스 입니다.") 367 | EXCEED_BUTTON_TEXT_LENGTH = (55813, "최대 버튼 글자수 초과") 368 | INVALID_MESSAGE_BASE_BUTTONS = (55814, "버튼 형식 오류") 369 | MESSAGE_BODY_FILE_NOT_FOUND = (55815, "존재하지 않는 File이거나 usageType 오류") 370 | MISMATCHED_SUGGESTIONS_COUNT = (55816, "Empty suggestions array 허용 안함") 371 | INVALID_DEST_PHONE_NUMBER = (55817, "수신 번호 형식 오류") 372 | INVALID_MESSAGE_BASE_ID = (55818, "메세지베이스 ID가 존재하지 않음") 373 | INVALID_CHATBOT_ID = (55819, "챗봇ID가 존재하지 않음") 374 | REVOKED_MESSAGE = (55820, "Webhook Revoked 메시지") 375 | ETC_TIME_OUT = (55821, "전송 성공 불확실함 (revocation fail 등)") 376 | CANCELED_MESSAGE = (55822, "메시지 취소되어, 전송안됨") 377 | INVALID_CHATBOT_YN = (55822, "양방향 서비스 사용불가") 378 | MISMATCHED_SUGGESTED_CHIP_LIST_COUNT = (55823, "Empty suggestedChipList array 허용 안함") 379 | CHIPLIST_NOT_ALLOWED = (55824, "칩리스트 필드를 사용할 수 없습니다.") 380 | BUTTONS_REPLY_NOT_ALLOWED = (55825, "버튼 필드에 Reply를 사용할 수 없습니다.") 381 | MISMATCHED_CAROUSEL_BUTTON_COUNT = (55880, "메시지카드 버튼 갯수 상이") 382 | MISMATCHED_CHIP_LIST_COUNT = (55881, "칩리스트 개수 초과") 383 | CHIP_LIST_NOT_ALLOWED = (55882, "ChipList 발송 가능하지 않음") 384 | INVALID_REPLY_ID = (55883, "유효한 replyId 아님") 385 | MISSING_REPLY_ID = (55884, "replyId 값 누락 됨") 386 | INVALID_USER_CONTACT_FOR_SESSION_MESSAGE = (55885, "replyId 와 일치하는 수신번호가 아님") 387 | INVALID_CHATBOT_ID_FOR_SESSION_MESSAGE = (55886, "replyId 와 일치하는 Chatbot ID 아님") 388 | INVALID_MESSAGE_BASE_PRODUCT_CODE_FOR_SESSION_MESSAGE = (55887, "messagebase 상품 코드가 세션 메시지 가능하지 않음") 389 | NOT_ALLOWED_CHATBOT_FOR_SESSION_MESSAGE = (55888, "Chatbot 이 세션 메시지 가능하지 않음") 390 | NON_RETRYABLE_ERROR_CAUSED_BY_INVALID_MESSAGE = ( 391 | 55900, 392 | "잘못된 메시지 형식으로 인해 발송 실패되었고 재시도 가능하지 않음 (삼성 에러 42001 ~ 42514)", 393 | ) 394 | REVOCATION_FAILED = (56002, "메시지 회수 실패 (삼성 에러 41002)") 395 | EXPIRED_BEFORE_SESSION_ESTABLISHMENT = (56007, "RCS 세션 연결 전 만료되어 발송 실패 (삼성 에러 41007)") 396 | BACKEND_ERROR = (59002, "Backend(삼성 MaaP G/W) 서버 내부 에러") 397 | BACKEND_TIMEOUT = (59003, "Backend(삼성 MaaP G/W) 서버 타임 아웃 발생") 398 | ETC_ERROR = (59999, "기타 정의되지 않은 Error (Webhook Cancelled 메시지 등)") 399 | NOT_FOUND_HANDLER = (51900, "잘못된 요청입니다.") 400 | SAMSUNG_MAA_P_CONNECT_IF_ERROR = (51901, "삼성 MaaP Gateway NB API 연동 에러") 401 | SAMSUNG_MAA_P_SERVICE_IF_ERROR = (51902, "삼성 MaaP Registry Chatbot API 연동 에러") 402 | CAPRI_IF_ERROR = (51903, "Capri 연동 에러") 403 | WEBHOOK_EXECUTE_IMPOSIBLE_STATUS_FAILURE = (51904, "Webhook 처리 불가 상태 오류가 발생했습니다.") 404 | WEBHOOK_CDR_LOG_WRITING_FAILURE = (51905, "Webhook 메시지 전송 과금 이력 작성을 실패했습니다.") 405 | INVALID_WEBHOOK_URL = (51906, "잘못된 Webhook Url 입니다.") 406 | EXPIRED_WEBHOOK_MESSAGE = (51907, "만료된 메시지 입니다.") 407 | EXCEED_RETRY_COUNT_TO_SEND_MESSAGE = (51908, "재시도 횟수 초과로 인해 메시지 전송을 실패했습니다.") 408 | NON_EXISTING_WEBHOOK_MESSAGE = (51909, "Webhook 발송 메시지가 존재하지 않습니다.") 409 | NON_EXISTING_WEBHOOK_GW_VENDOR = (51910, "Webhook 발송 중계사 정보가 존재하지 않습니다.") 410 | NO_SUBSCRIPTION = (51911, "계약관계가 없습니다.") 411 | MAAP_SIMULATER_FAIL = (51912, "삼성 시뮬레이터 연동 오류") 412 | FAILURE_IN_PREPERATION_FOR_SENDING_WEBHOOK = (51912, "Webhook 발송 준비 수행 중 오류가 발생 했습니다.") 413 | FAILURE_IN_UPDATING_WEBHOOK_SENDING_RESULT_STATE = (51913, "Webhook 발송 결과 상태 갱신 중 오류가 발생 했습니다.") 414 | FAILURE_IN_UPDATING_CDR_RESULT_STATE = (51914, "CDR 생성 결과 상태 갱신 중 오류가 발생 했습니다.") 415 | FAILURE_IN_UPDATING_COMPLETION_RESULT_STATE = (51915, "완료 처리 결과 상태 갱신 중 오류가 발생했습니다.") 416 | FAILURE_IN_UPDATING_EXPIRATION_RESULT_STATE = (51916, "만료 처리 결과 상태 갱신 중 오류가 발생했습니다.") 417 | WEBHOOK_MSG_LOG_CREATION_ERROR = (51917, "메시지 이력 생성 작업중 오류가 발생했습니다.") 418 | WEBHOOK_MSG_LOG_CREATION_FAILURE = (51918, "메시지 이력 생성 작업을 실패했습니다.") 419 | INVALID_WEBHOOK_RECEIVE_REQUEST_ERROR = (51919, "잘못된 Webhook 요청 오류 ") 420 | NON_EXISTING_CHATBOT_FOR_REQUEST = (51920, "요청 양뱡향 챗봇에 대한 정보가 존재하지 않습니다") 421 | NON_USABLE_CHATBOT_STATE_ERROR = (51921, "사용 불가 챗봇 상태 오류입니다") 422 | NON_EXISTING_GW_VENDOR_FOR_REQUEST = (51922, "요청 양방향 챗봇에 대한 양방향 중계사 정보가 존재하지 않습니다.") 423 | NON_DEFINITION_MO_MESSAGE_URL_FOR_CHATBOT = (51923, "챗봇 Mo 발송 Url 정보가 미 정의 상태 입니다.") 424 | WEBHOOK_GATEWAY_EXECUTION_ERROR = (51924, "Webhook 수신 Gateway 수행 오류") 425 | NOT_ALLOWED_REQUEST_EVENT_ERROR = (51925, "미 허용 Webhook 이벤트 요청 오류") 426 | WEBHOOK_RECEIVE_EXECUTION_ERROR = (51926, "Webhook 수신 처리 수행 오류") 427 | WEBHOOK_RECEIVE_ASYNC_EXECUTION_ERROR = (51927, "Webhook 수신 처리 비동기 수행 오류") 428 | NON_DEFINITION_WEBHOOK_URL_FOR_GW_VENDOR_CID = (51928, "중계사 CID Webhook 발송 Url 정보가 미 정의 상태 입니다.") 429 | NON_TARGET_GW_VENDOR_FOR_CDR_LOG = (51929, "과금 미 처리 대상 중계사 입니다.") 430 | NON_RECEIVED_WEBHOOK_COMMAND_ERROR_LOG = (51930, "수행 명령 객체 미 전달 오류입니다.") 431 | MO_MESSAGE_REGISTRATION_ERROR = (51931, "Mo 메시지 DB 등록 오류가 발생했습니다.") 432 | MO_MESSAGE_REGISTRATION_FAILUER = (51932, "Mo 메시지 DB 등록 작업을 실패했습니다.") 433 | AUTO_REPLY_MESSAGE_SENDING_ERROR = (51933, "자동 응답 메시지 발송 수행 오류가 발생했습니다.") 434 | NON_SERVICE_SUPPORTED_ERROR = (51934, "처리 미 대상 서비스 입니다.") 435 | SAMSUNG_MAA_P_CORE_FILE_SERVER_CONNECTION_ERROR = (51935, "삼성 MaaP Core 파일 서버 연결 오류가 발생했습니다.") 436 | MESSAGE_FILE_MESSAGE_EVENT_FILE_DOWNLOAD_ERROR_1 = (51936, "파일 메시지 이벤트의 파일 메시지 다운로드 수행 오류가 발생했습니다.") # noqa: E501 437 | MESSAGE_FILE_MESSAGE_EVENT_FILE_DOWNLOAD_ERROR_2 = (51937, "파일 메시지 이벤트의 파일 메시지 다운로드 수행 오류가 발생했습니다.") # noqa: E501 438 | MESSAGE_FILE_MESSAGE_REGISTRATION_TO_DB_ERROR = (51938, "파일 메시지 이벤트의 파일 정보 DB 등록 오류가 발생했습니다.") # noqa: E501 439 | MESSAGE_FILE_MESSAGE_REGISTRATION_TO_DB_FAILURE = (51939, "파일 메시지 이벤트의 파일 정보 DB 등록 작업을 실패 했습니다.") # noqa: E501 440 | WEBHOOK_SCHEDULER_ASYNC_EXECUTION_ERROR = (51950, "Webhook 스케줄러 비동기 수행 오류.") 441 | WEBHOOK_SCHEDULER_DB_EXECUTION_ERROR = (51951, "Webhook 스케줄러 DB 수행 오류.") 442 | WEBHOOK_SCHEDULER_DB_EXECUTION_FAILUER = (51952, "Webhook 스케줄러 DB 수행 실패.") 443 | WEBHOOK_SCHEDULER_PROCESS_EXECUTION_ERROR = (51953, "Webhook 스케줄러 프로세스 수행 오류.") 444 | WEBHOOK_SCHEDULER_PROCESS_EXECUTION_FAILUER = (51954, "Webhook 스케줄러 프로세스 수행 실패.") 445 | WEBHOOK_SCHEDULER_PROCESSOR_EXECUTION_ERROR = (51955, "Webhook 스케줄러 프로세서 수행 오류.") 446 | WEBHOOK_SCHEDULER_PROCESSOR_EXECUTION_FAILUER = (51956, "Webhook 스케줄러 프로세서 수행 실패.") 447 | NON_EXISTING_MO_MESSAGE_ERROR = (51957, "Mo 메시지가 존재하지 않습니다.") 448 | 449 | 450 | class RcsBizCenterErrorCodeEnum(TupleEnum): 451 | """ 452 | Rcs Biz Center Enum of error codes. 453 | """ 454 | 455 | MISSING_AUTHORIZATION_HEADER = (61001, "Authorization 헤더 파라미터 누락") 456 | MISSING_TOKEN = (61002, "Authorization 헤더 값(Token) 누락") 457 | INVALID_TOKEN = (61003, "유효하지 않은 Token") 458 | TOKEN_HAS_EXPIRED = (61004, "Token 만료") 459 | INVALID_CLIENT_ID = (61005, "유효하지 않은 client id") 460 | INVALID_SECRET_KEY = (61006, "유효하지 않은 secret key") 461 | NO_BRAND_PERMISSION = (63001, "브랜드에 대한 권한 없음") 462 | MISSING_X_RCS_BRANDKEY_HEADER = (64001, "X-RCS-BrandKey 헤더 누락") 463 | INVALID_BRAND_KEY = (64002, "X-RCS-BrandKey 의 Brand Key 오류") 464 | INVALID_BRANDID_ON_PATH_PARAMETER = (64101, "URL 내 Brand ID 오류") 465 | INVALID_AGENCYID_ON_PATH_PARAMETER = (64102, "URL 내 Agency ID 오류") 466 | INVALID_CORPREGNUM_ON_PATH_PARAMETER = (64103, "URL 내 사업자등록번호 오류") 467 | INVALID_PERSONID_ON_PATH_PARAMETER = (64104, "URL 내 Person ID 오류") 468 | INVALID_CHATBOTID_ON_PATH_PARAMETER = (64105, "URL 내 chatbot ID 오류") 469 | INVALID_MESSAGEBASEID_ON_PATH_PARAMETER = (64106, "URL 내 messagebase ID 오류") 470 | INVALID_MESSAGEBASEFORMID_ON_PATH_PARAMETER = (64107, "URL 내 messagebaseform ID 오류") 471 | INVALID_QUERY_PARAMETER = (64201, "유효하지 않은 Query 파라미터 : (해당 파라미터)") 472 | INVALID_QUERY_PARAMETER_VALUE = (64202, "Query 파라미터 값 오류 : (오류발생 값)") 473 | QUERY_PARAMTER_REQUIRED = (64203, "필수 Query 파라미터 누락 : (누락된 파라미터)") 474 | MISSING_BODY_DATA = (64301, "Body Data 누락") 475 | INVALID_JSON_FORMAT = (64302, "Body Data JSON 형식 오류") 476 | INVALID_TYPE_OF_ATTRIBUTE = (64303, "Attribute type 오류 : (오류 발생 attribute)") 477 | OVER_SPECIFIED_SIZE = (64304, "지정된 사이즈 초과 : (사이즈 초과된 attribute)") 478 | MISSING_CERTIFICATION_DOCUMENT = (64305, "발신번호 등록 시 통신서비스이용증명원 파일 누락") 479 | EXCEED_MDN_REGISTRATION_QUANTITY = (64306, "발신번호 등록 시 발신번호 등록 개수 초과") 480 | MISSING_MDN = (64307, "발신번호 등록 시 발신번호 누락") 481 | INVALID_MDN_FORMAT = (64308, "발신번호 등록 시 발신번호 형식 오류") 482 | MISSING_CHATBOT_NAME = (64309, "발신번호 등록 시 챗봇이름 누락") 483 | INVALID_DISPLAY_FORMAT = (64310, "발신번호 등록 시 display 설정 형식 오류") 484 | INVALID_SMSMO_FORMAT = (64311, "발신번호 등록 시 smsmo 형식 오류") 485 | MISSING_MESSAGEBASEFORMID = (64312, "템플릿 등록 시 템플릿 양식 ID 누락") 486 | INVALID_MESSAGEBASEFORMID = (64313, "템플릿 등록 시 템플릿 양식 ID 오류") 487 | MISSING_TEMPLATE_NAME = (64314, "템플릿 등록 시 템플릿명 누락") 488 | MISSING_BRANDID = (64315, "템플릿 등록 시 브랜드 ID 누락") 489 | INVALID_BRANDID = (64316, "템플릿 등록 시 브랜드 ID 오류") 490 | INVALID_AGENCYID = (64317, "템플릿 등록 시 대행사 ID 오류") 491 | INVALID_FORMATTEDSTRING_FORMAT = (64318, "템플릿 등록 시 formattedString 형식 오류") 492 | 493 | 494 | class KTErrorCodeEnum(TupleEnum): 495 | """ 496 | KT 중계사 Enum of error codes. 497 | """ 498 | 499 | MISSING_AUTHORIZATION_HEADER = (70001, "HTTP 요청 헤더에 인증 정보가 없습니다.") 500 | MISSING_TOKEN = (70002, "인증정보에 토큰이 없습니다.") 501 | INVAILD_TOKEN = (70003, "토큰이 일치하지 않습니다.") 502 | TOKEN_HAS_EXPIRED = (70004, "토큰이 만료되었습니다.") 503 | AUTHORIZATION_ERROR = (70005, "인증에 실패하였습니다.") 504 | INVALID_CLIENT_ID = (70006, "요청된 계정 정보를 찾을 수 없습니다(BP ID)") 505 | INVALID_SENDER_ID = (70007, "요청된 중계사 전송 계정을 찾을 수 없습니다(RCS ID)") 506 | INVALID_PASSWORD = (70008, "잘못된 패스워드") 507 | NON_ALLOWED_IP = (70009, "접근 허용된 IP가 아닙니다 ") 508 | SYSTEM_ERROR = (71001, "시스템 에러") 509 | IO_ERROR = (71002, "IO 에러 발생") 510 | DUPLICATION_ERROR = (71003, "중복 Key 오류") 511 | PARAMETER_ERROR = (71004, "요청 파라미터 형식 오류") 512 | JSON_PARSING_ERROR = (71005, "요청 Body JSON 파싱 에러") 513 | DATA_NOT_FOUND = (71006, "데이터를 찾을 수 없음") 514 | NOT_FOUND_HANDLER = (71007, "잘못된 요청입니다.") 515 | MISSING_MANDATORY_PARAMETER = (71008, "필수 파라미터가 누락되었습니다.") 516 | INVALID_DATA_STATE = (71009, "잘못된 데이터 상태입니다.") 517 | MAAP_FE_API_ERROR = (71010, "MAAP-FE API 연동 에러") 518 | KISA_GW_ERROR = (71011, "KISA GW 연동 에러") 519 | INVALID_STATE = (72100, "메시지 전송을 할 수 없는 상태입니다. (서버의 요청 거부)") 520 | COMMON_INFO_ERROR = (72101, "공통 메시지 정보가 부정확하거나 누락된 필수 항목이 있습니다.") 521 | LEGACY_INFO_ERROR = (72102, "Legacy 메시지 정보가 부정확하거나 누락된 필수 항목이 있습니다.") 522 | RCS_INFO_ERROR = (72103, "RCS 메시지 정보가 부정확하거나 누락된 필수 항목이 있습니다.") 523 | MESSAGE_TPS_EXCEEDED = (72104, "RCS 메세지 TPS가 초과되었습니다.") 524 | MESSAGE_QUOTA_EXCEEDED = (72105, "RCS 메세지 Quota가 초과되었습니다.") 525 | MESSAGE_MEDIA_NOT_FOUND = (72106, "미디어 파일을 찾을수 없습니다.") 526 | INVALID_MEDIA_FILE_TYPE = (72107, "요청을 처리할 수 없는 파일 유형입니다.") 527 | INVALID_MESSAGE_TYPE = (72108, "메시지 형식오류(메시지 규격에 맞지 않을 경우)") 528 | INVALID_CONTENT_TYPE = (72109, "올바른 컨텐츠가 아님") 529 | EXCEEDED_NUMBER_OF_CONTENTS = (72110, "동보 갯수초과 (Agent 내부)") 530 | DATA_TYPE_ERROR = (72111, "데이터 형식 오류 (Agent 내부)") 531 | ATTACHED_FILE_ERROR = (72112, "첨부파일 오류 (Agent 내부)") 532 | FILE_UPLOAD_ERROR = (72113, "파일 업로드 실패") 533 | CONVERT_TIMEOUT = (72114, "Convert 타입 아웃") 534 | ATTACHED_FILE_SIZE_ERROR = (72115, "첨부파일 용량 오류 ") 535 | WEBFILE_DOWNLOAD_ERROR = (72116, "웹파일 다운로드 오류") 536 | EXCEEDED_MESSAGE_CONTENT_SIZE = (72117, "메시지가 Overflow 되어 받지 못함") 537 | SUB_TYPE_ERROR = (72118, "Sub Type 오류 (전송 실패)") 538 | INVALIED_DATA_TYPE = (72119, "잘못된 Data Type인 경우") 539 | MESSAGE_FORMAT_ERROR = (72120, "메시지 포맷 오류") 540 | EXCEED_CONTENT_SIZE = (72121, "컨텐츠 크기가 커서 처리할 수 없음") 541 | INVALID_CHATBOT_PERMISSION = (72122, "계약관계가 없거나 해당 챗봇이 존재하지 않음") 542 | INVALID_SERVICE_TYPE = (72123, "올바른 메시지 서비스 타입이 아님") 543 | INVALID_SERVICE_PERMISSION = (72124, "해당 메시지 서비스 타입에 권한이 없어 전송 불가") 544 | INVALID_EXPIRY_OPTION = (72125, "올바른 메시지 Expiry Option이 아님") 545 | INVALID_HEADER = (72126, "올바른 메시지 Header가 아님") 546 | INVALID_FOOTER = (72127, "올바른 메시지 Footer가 아님") 547 | MESSAGE_SMS_QUOTA_EXCEEDED = (72128, "SMS 발송 수량 초과") 548 | MESSAGE_LMS_QUOTA_EXCEEDED = (72129, "LMS 발송 수량 초과") 549 | MESSAGE_MMS_QUOTA_EXCEEDED = (72130, "MMS 발송 수량 초과") 550 | MESSAGE_TMPLT_QUOTA_EXCEEDED = (72131, "TMPLT 발송 수량 초과") 551 | MESSAGE_CHAT_QUOTA_EXCEEDED = (72132, "CHAT 발송 수량 초과") 552 | CHIPLIST_NOT_ALLOWED = (72133, "칩리스트를 사용할 수 없습니다.") 553 | INVALID_CHATBOT_CHAT_PERMISSION = (72134, "양방향 챗봇에 대한 권한이 없습니다.") 554 | NOT_FOUND_CORP_CHAT = (72135, "양방향 기업 데이터가 존재하지 않습니다.") 555 | INVALID_CHAT_MESSAGE_PERMISSION = (72136, "양방향 서비스에 대한 권한이 없습니다.") 556 | NOT_FOUND_REPLY_ID = (72137, "Reply ID가 존재하지 않습니다.") 557 | EXPIRED_REPLY_ID = (72138, "Reply ID 유효시간이 만료되었습니다.") 558 | UNAVAILABLE_SERVICE = (73001, "가입되지 않은 상품 발송") 559 | UNSUBSCRIBED_LEGACY_SERVICE = (73002, "Legacyinfo가 있지만 청약 정보내 크로샷 ID가 없는 경우") 560 | MESSAGE_CONTENT_SPAM = (74001, "메시지 내용 스팸") 561 | SENDER_NUMBER_SPAM = (74002, "발신자 스팸") 562 | RECEIPIENT_NUMBER_SPAM = (74003, "착신자 스팸") 563 | CALLBACK_NUMBER_SPAM = (74004, "회신 번호 스팸") 564 | SAME_MESSAGE_LIMIT = (74005, "동일 메시지 제한") 565 | SAME_RECEIPIENT_NUMBER_LIMIT = (74006, "동일 착신번호 제한") 566 | NUMBER_THEFT_BLOCK = (74007, "번호도용/변작방지 차단") 567 | CALLBACK_NUMBER_BLOCK = (74008, "미등록 회신번호 차단") 568 | NUMBER_RULE_VIOLATION_BLOCK = (74009, "번호세칙 위반 차단") 569 | NPDB_USER_SEND_FAIL = (75001, "번호 이동된 가입자 (전송 실패)") 570 | THERE_IS_NO_SUBSCRIBER = (75002, "가입자 없음") 571 | CAPRI_AND_NPDB_ERROR = (75003, "NPDB 오류") 572 | THERE_IS_NO_END_USER = (75004, "End-User(사용자) 존재하지 않음, 해지, 정지") 573 | RECEIPIENT_NUMBER_ERROR = (75005, "착신번호 에러 (자리수에러, 없는 번호)") 574 | SENDER_NUMBER_ERROR = (75006, "발신번호 오류") 575 | NO_RCS_CAPABILITY = (75007, "자사 고객이지만, RCS메시지를 수신할 수 있는 가입자가 아닙니다.") 576 | INVALID_USER_CONTACT = (75008, "잘못된 규격의 착신번호") 577 | CAPRI_INTERFACE_ERROR = (76001, "CAPRI 연동 실패") 578 | SCHEDULE_MANAGER_INTERNAL_ERROR = (76002, "Schedule Manager 내부 에러") 579 | NOT_FOUND_RCS_SUBSCRIBER = (76003, "RCS 가입 정보 없음") 580 | XROSHOT_SENDER_INTERNAL_ERROR = (76004, "Xroshot Sender 내부 에러") 581 | XROSHOT_MANAGER_INTERNAL_ERROR = (76005, "Xroshot Manager 내부 에러") 582 | EXPIRED_MESSAGE_RECEIVED_TIME = (77001, "Legacy : 레포트 수신 시간 만료 (메시지 전송 후, 24시간 레포트 못받는 경우) RCS 3일") # noqa: E501 583 | INVALIED_MESSAGE_SEQUENCE = (77002, "Message Sequence Number가 틀린 경우") 584 | NON_EXISTING_WEBHOOK_MESSAGE = (77003, "Webhook 발송 메시지가 존재하지 않습니다") 585 | INVALID_WEBHOOK_MESSAGE_1 = (77004, "잘못된 Webhook 발송 메시지입니다.") 586 | NON_EXISTING_WEBHOOK_CORPORATION = (77005, "Webhook 회사 정보가 존재하지 않습니다.") 587 | WEBHOOK_MSG_LOG_WRITING_FAILURE = (77006, "Webhook 메시지 전송 이력 정보 작성을 실패 했습니다.") 588 | DISABLED_SEND_WEBHOOK_MSG = (77007, "해당 RCS ID의 webhook 발송 설정이 꺼져있습니다.") 589 | WEBHOOK_FAILURE_STATUS = (77008, "Webhook 상태 코드 실패 수신") 590 | INVALID_WEBHOOK_MESSAGE_2 = (77009, "Webhook 메시지가 유효하지 않습니다.") 591 | INVALID_WEBHOOK_EVENT_TYPE = (77010, "Webhook EventType이 유효하지 않습니다.") 592 | INVALID_WEBHOOK_MESSAGE_STATUS = (77011, "Webhook Message Status type이 유효하지 않습니다.") 593 | NOT_FOUND_AGENCY = (77012, "대행사 정보가 존재하지 않습니다.") 594 | NON_EXISTING_MO_MESSAGE = (77013, "MO 메시지가 존재하지 않습니다") 595 | UNSUPPORT_ERROR = (78001, "미지원 단말") 596 | CALLING = (78002, "통화중") 597 | DEVICE_NO_RESPONSE = (78003, "무응답 (단말기 무응답)") 598 | DEVICE_POWER_OFF = (78004, "단말기 전원 꺼짐") 599 | SHADED_AREA = (78005, "음영 지역") 600 | DEVICE_MESSAGE_FULL = (78006, "단말 메시지 Full") 601 | SMS_FORWARD_EXCEED = (78007, "SMS 착신전환 회수 초과") 602 | INVALID_SUBSCRIBER_NAME = (78008, "한글/영문외의 가입자 일 경우") 603 | NO_CALLBACK_URL_USER = (78009, "CallbackURL 사용자 아님") 604 | INVALID_DEVICE_ERROR = (78010, "비가용폰 오류") 605 | RETRY_COUNT_EXCEEDED = (79001, "재시도 횟수를 초과하였습니다.") 606 | CONCURRENT_MAX_REQUEST_EXCEEDED = (79002, "최대 동시 접속 수 초과") 607 | RCS_ID_MISMATCHED = (79003, "RCS ID 불일치 오류") 608 | INVALID_QUERY_REQUEST = (79004, "잘못된 메시지 결과 요청") 609 | EXCEED_QUERY_LIMIT_COUNT = (79005, "메시지 조회 횟수 한도 초과") 610 | REQUEST_QUERY_EXECUTION_FAILURE = (79006, "요청 수행 실패") 611 | DUPLICATED_QUERY_ID = (79007, "요청 ID 중복 오류") 612 | INVAILD_REQUEST_ERROR = (79008, "유효하지 않는 서버로의 요청") 613 | RESERVED = (79009, "Reserved") 614 | INVALID_RESULT_POLICY = (79010, "결과 메시지 조회 정책 미 유효") 615 | NON_EXIST_QUERY_ID = (79011, "요청 ID 미 존재 오류") 616 | KT_MAA_P_FE_FAIL = (79994, "KT 통신사 장애로 실패") 617 | SKT_MAA_P_FE_FAIL = (79995, "SKT 통신사 장애로 실패") 618 | LGU_MAA_P_FE_FAIL = (79996, "LGU 통신사 장애로 실패") 619 | TIME_OUT = (79997, "전송 실패 (expiryOption TimeOut 등)") 620 | ETC_TIME_OUT = (79998, "전송 성공 불확실함 (expiryOption TimeOut 등)") 621 | UNKNOWN_ERROR = (79999, "Unknown Error") 622 | 623 | 624 | # class RcsException(Exception): 625 | # """ 626 | # Base class for all RCSErrorCode exceptions. 627 | # """ 628 | 629 | # def __init__(self, message): 630 | # self.message = message 631 | # super().__init__(message) 632 | 633 | # def __str__(self): 634 | # return self.message 635 | --------------------------------------------------------------------------------