├── .dockerignore
├── .github
├── renovate-bot.json5
├── renovate.json5
├── renovate
│ ├── auto-merge.json5
│ ├── commit-message.json5
│ ├── gomod.json5
│ ├── labels.json5
│ ├── regex-manager.json5
│ └── semantic-commits.json5
└── workflows
│ ├── main.yml
│ ├── master.yml
│ ├── release.yml
│ ├── renovate.yml
│ └── swagger-change.yml
├── .gitignore
├── .golangci.yml
├── .hadolint.yaml
├── .spectral.yml
├── .tbls.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── cmd
├── conf.go
├── config.go
├── file.go
├── healthcheck.go
├── migrate.go
├── migrate_v2_to_v3.go
├── root.go
├── serve.go
├── serve_wire.go
├── stamp.go
├── version.go
└── wire_gen.go
├── codecov.yml
├── compose.yaml
├── dev
├── .gitignore
├── Caddyfile
├── Dockerfile-frontend
├── bin
│ ├── bot_debugger.go
│ ├── down-test-db.sh
│ ├── gen_ec_pem.go
│ └── up-test-db.sh
└── es_jvm.options
├── docs
├── dbSchema
│ ├── README.md
│ ├── archived_messages.md
│ ├── bot_event_logs.md
│ ├── bot_join_channels.md
│ ├── bots.md
│ ├── channel_events.md
│ ├── channel_latest_messages.md
│ ├── channels.md
│ ├── clip_folder_messages.md
│ ├── clip_folders.md
│ ├── devices.md
│ ├── dm_channel_mappings.md
│ ├── external_provider_users.md
│ ├── files.md
│ ├── files_acl.md
│ ├── files_thumbnails.md
│ ├── message_reports.md
│ ├── messages.md
│ ├── messages_stamps.md
│ ├── migrations.md
│ ├── oauth2_authorizes.md
│ ├── oauth2_clients.md
│ ├── oauth2_tokens.md
│ ├── ogp_cache.md
│ ├── pins.md
│ ├── r_sessions.md
│ ├── soundboard_items.md
│ ├── stamp_palettes.md
│ ├── stamps.md
│ ├── stars.md
│ ├── tags.md
│ ├── unreads.md
│ ├── user_group_admins.md
│ ├── user_group_members.md
│ ├── user_groups.md
│ ├── user_profiles.md
│ ├── user_role_inheritances.md
│ ├── user_role_permissions.md
│ ├── user_roles.md
│ ├── user_settings.md
│ ├── users.md
│ ├── users_private_channels.md
│ ├── users_subscribe_channels.md
│ ├── users_tags.md
│ └── webhook_bots.md
├── deployment.md
├── development.md
├── favicon-16x16.png
├── favicon-32x32.png
├── swagger.yaml
└── v3-api.yaml
├── event
└── topic.go
├── go.mod
├── go.sum
├── main.go
├── migration
├── current.go
├── doc.go
├── migrate.go
├── v1.go
├── v10.go
├── v11.go
├── v12.go
├── v13.go
├── v14.go
├── v15.go
├── v16.go
├── v17.go
├── v18.go
├── v19.go
├── v2.go
├── v20.go
├── v21.go
├── v22.go
├── v23.go
├── v24.go
├── v25.go
├── v26.go
├── v27.go
├── v28.go
├── v29.go
├── v2tov3
│ └── v2tov3.go
├── v3.go
├── v30.go
├── v31.go
├── v32.go
├── v33.go
├── v34.go
├── v35.go
├── v36.go
├── v37.go
├── v38.go
├── v39.go
├── v4.go
├── v5.go
├── v6.go
├── v7.go
├── v8.go
└── v9.go
├── model
├── bot.go
├── bot_test.go
├── channels.go
├── channels_test.go
├── clip.go
├── devices.go
├── devices_test.go
├── files.go
├── files_test.go
├── json.go
├── message_report.go
├── message_report_test.go
├── message_stamp.go
├── message_stamp_test.go
├── messages.go
├── messages_test.go
├── model.go
├── oauth2.go
├── oauth2_test.go
├── ogp.go
├── pins.go
├── pins_test.go
├── rbac.go
├── rbac_test.go
├── session.go
├── soundboard.go
├── soundboard_test.go
├── stamp_palettes.go
├── stamps.go
├── stamps_test.go
├── stars.go
├── stars_test.go
├── tags.go
├── tags_test.go
├── user_group.go
├── user_group_test.go
├── user_settings.go
├── users.go
├── users_test.go
├── uuids.go
├── webhooks.go
└── webhooks_test.go
├── repository
├── bot.go
├── channel.go
├── clip.go
├── device.go
├── errors.go
├── errors_test.go
├── file.go
├── gorm
│ ├── bot.go
│ ├── channel.go
│ ├── channel_test.go
│ ├── clip.go
│ ├── clip_test.go
│ ├── device.go
│ ├── device_test.go
│ ├── errors.go
│ ├── file.go
│ ├── file_test.go
│ ├── message.go
│ ├── message_report.go
│ ├── message_test.go
│ ├── oauth2.go
│ ├── ogp_cache.go
│ ├── pin.go
│ ├── pin_test.go
│ ├── repository.go
│ ├── repository_test.go
│ ├── soundboard.go
│ ├── stamp.go
│ ├── stamp_palette.go
│ ├── stamp_palette_test.go
│ ├── stamp_test.go
│ ├── star.go
│ ├── star_test.go
│ ├── tag.go
│ ├── tag_test.go
│ ├── user.go
│ ├── user_group.go
│ ├── user_group_test.go
│ ├── user_role.go
│ ├── user_role_test.go
│ ├── user_settings.go
│ ├── user_test.go
│ ├── webhook.go
│ └── webhook_test.go
├── message.go
├── message_report.go
├── mock_repository
│ ├── mock_bot.go
│ ├── mock_channel.go
│ ├── mock_file.go
│ ├── mock_message.go
│ ├── mock_pin.go
│ ├── mock_soundboard.go
│ ├── mock_tag.go
│ ├── mock_user.go
│ └── mock_user_role.go
├── oauth2.go
├── ogp_cache.go
├── pin.go
├── repository.go
├── soundboard.go
├── stamp.go
├── stamp_palette.go
├── star.go
├── tag.go
├── user.go
├── user_group.go
├── user_role.go
├── user_settings.go
├── user_test.go
└── webhook.go
├── router
├── auth
│ ├── github.go
│ ├── google.go
│ ├── oidc.go
│ ├── provider.go
│ ├── slack.go
│ └── traq.go
├── config.go
├── consts
│ ├── headers.go
│ ├── keys.go
│ ├── mime_types.go
│ ├── parameters.go
│ └── stamp_type.go
├── extension
│ ├── context.go
│ ├── ctxkey
│ │ └── extension.go
│ ├── error_handler.go
│ ├── herror
│ │ ├── 500.go
│ │ └── errors.go
│ ├── logger.go
│ ├── precond.go
│ └── precond_test.go
├── middlewares
│ ├── access_control.go
│ ├── access_logging.go
│ ├── body_limit.go
│ ├── gzip.go
│ ├── no_login.go
│ ├── param_retriever.go
│ ├── precond.go
│ ├── recovery.go
│ ├── request_counter.go
│ ├── request_id.go
│ ├── server_version.go
│ └── user_authenticate.go
├── oauth2
│ ├── authorization_endpoint.go
│ ├── authorization_endpoint_test.go
│ ├── oauth2.go
│ ├── oauth2_test.go
│ ├── oidc.go
│ ├── revoke_token_endpoint.go
│ ├── revoke_token_endpoint_test.go
│ ├── token_endpoint.go
│ └── token_endpoint_test.go
├── router.go
├── router_wire.go
├── session
│ ├── gorm.go
│ ├── memory.go
│ └── session.go
├── utils
│ ├── common.go
│ ├── process_image.go
│ ├── replace_mapper.go
│ └── validator.go
├── v1
│ ├── files.go
│ ├── files_test.go
│ ├── public.go
│ ├── public_test.go
│ ├── responses.go
│ ├── router.go
│ └── router_test.go
├── v3
│ ├── activity.go
│ ├── activity_test.go
│ ├── bots.go
│ ├── bots_test.go
│ ├── channels.go
│ ├── channels_test.go
│ ├── clients.go
│ ├── clients_test.go
│ ├── clips.go
│ ├── clips_test.go
│ ├── files.go
│ ├── files_test.go
│ ├── messages.go
│ ├── messages_test.go
│ ├── ogp.go
│ ├── public.go
│ ├── public_test.go
│ ├── qall.go
│ ├── responses.go
│ ├── router.go
│ ├── router_test.go
│ ├── sessions.go
│ ├── sessions_test.go
│ ├── stamp_palettes.go
│ ├── stamp_palettes_test.go
│ ├── stamps.go
│ ├── stamps_test.go
│ ├── star.go
│ ├── star_test.go
│ ├── tags.go
│ ├── tags_test.go
│ ├── user_groups.go
│ ├── user_groups_test.go
│ ├── user_settings.go
│ ├── user_settings_test.go
│ ├── users.go
│ ├── users_test.go
│ ├── utils.go
│ ├── webhooks.go
│ ├── webhooks_test.go
│ ├── webrtc.go
│ ├── webrtc_test.go
│ └── ws.go
└── wire_gen.go
├── service
├── bot
│ ├── event
│ │ ├── dispatcher.go
│ │ ├── dispatcher_http.go
│ │ ├── dispatcher_impl.go
│ │ ├── dispatcher_test.go
│ │ ├── dispatcher_ws.go
│ │ ├── events.go
│ │ ├── mock_event
│ │ │ └── mock_dispatcher.go
│ │ └── payload
│ │ │ ├── common.go
│ │ │ ├── ev_bot_message_stamps_updated.go
│ │ │ ├── ev_channel_created.go
│ │ │ ├── ev_channel_topic_changed.go
│ │ │ ├── ev_direct_message_created.go
│ │ │ ├── ev_direct_message_deleted.go
│ │ │ ├── ev_direct_message_updated.go
│ │ │ ├── ev_joined.go
│ │ │ ├── ev_left.go
│ │ │ ├── ev_message_created.go
│ │ │ ├── ev_message_deleted.go
│ │ │ ├── ev_message_updated.go
│ │ │ ├── ev_ping.go
│ │ │ ├── ev_stamp_created.go
│ │ │ ├── ev_tag_added.go
│ │ │ ├── ev_tag_removed.go
│ │ │ ├── ev_user_activated.go
│ │ │ ├── ev_user_created.go
│ │ │ ├── ev_user_group_admin_added.go
│ │ │ ├── ev_user_group_admin_removed.go
│ │ │ ├── ev_user_group_created.go
│ │ │ ├── ev_user_group_deleted.go
│ │ │ ├── ev_user_group_member_added.go
│ │ │ ├── ev_user_group_member_removed.go
│ │ │ ├── ev_user_group_member_updated.go
│ │ │ └── ev_user_group_updated.go
│ ├── handler
│ │ ├── context.go
│ │ ├── ev_bot_joined.go
│ │ ├── ev_bot_joined_test.go
│ │ ├── ev_bot_left.go
│ │ ├── ev_bot_left_test.go
│ │ ├── ev_bot_ping_request.go
│ │ ├── ev_bot_ping_request_test.go
│ │ ├── ev_channel_created.go
│ │ ├── ev_channel_created_test.go
│ │ ├── ev_channel_topic_updated.go
│ │ ├── ev_channel_topic_updated_test.go
│ │ ├── ev_message_created.go
│ │ ├── ev_message_created_test.go
│ │ ├── ev_message_deleted.go
│ │ ├── ev_message_deleted_test.go
│ │ ├── ev_message_stamps_updated.go
│ │ ├── ev_message_stamps_updated_test.go
│ │ ├── ev_message_updated.go
│ │ ├── ev_message_updated_test.go
│ │ ├── ev_stamp_created.go
│ │ ├── ev_stamp_created_test.go
│ │ ├── ev_user_activated.go
│ │ ├── ev_user_activated_test.go
│ │ ├── ev_user_created.go
│ │ ├── ev_user_created_test.go
│ │ ├── ev_user_group_admin_added.go
│ │ ├── ev_user_group_admin_added_test.go
│ │ ├── ev_user_group_admin_removed.go
│ │ ├── ev_user_group_admin_removed_test.go
│ │ ├── ev_user_group_created.go
│ │ ├── ev_user_group_created_test.go
│ │ ├── ev_user_group_deleted.go
│ │ ├── ev_user_group_deleted_test.go
│ │ ├── ev_user_group_member_added.go
│ │ ├── ev_user_group_member_added_test.go
│ │ ├── ev_user_group_member_removed.go
│ │ ├── ev_user_group_member_removed_test.go
│ │ ├── ev_user_group_member_updated.go
│ │ ├── ev_user_group_member_updated_test.go
│ │ ├── ev_user_group_updated.go
│ │ ├── ev_user_group_updated_test.go
│ │ ├── ev_user_tag_added.go
│ │ ├── ev_user_tag_added_test.go
│ │ ├── ev_user_tag_removed.go
│ │ ├── ev_user_tag_removed_test.go
│ │ ├── handler_test.go
│ │ └── mock_handler
│ │ │ └── mock_context.go
│ ├── handlers.go
│ ├── service.go
│ ├── service_impl.go
│ └── ws
│ │ ├── config.go
│ │ ├── handler.go
│ │ ├── message.go
│ │ ├── metrics.go
│ │ ├── session.go
│ │ └── streamer.go
├── channel
│ ├── manager.go
│ ├── manager_impl.go
│ ├── manager_impl_test.go
│ ├── mock_channel
│ │ ├── mock_manager.go
│ │ └── mock_tree.go
│ ├── tree.go
│ ├── tree_impl.go
│ └── tree_impl_test.go
├── counter
│ ├── channel.go
│ ├── message.go
│ ├── online.go
│ ├── unread.go
│ └── user.go
├── exevent
│ └── stamp_throttler.go
├── fcm
│ ├── client.go
│ ├── impl.go
│ ├── null.go
│ └── vars.go
├── file
│ ├── animated_image.go
│ ├── manager.go
│ ├── manager_impl.go
│ ├── manager_impl_test.go
│ ├── meta_impl.go
│ └── utils.go
├── imaging
│ ├── config.go
│ ├── mks2013_filter.go
│ ├── mock_imaging
│ │ └── mock_processor.go
│ ├── processor.go
│ ├── processor_default.go
│ └── processor_default_test.go
├── message
│ ├── manager.go
│ ├── manager_impl.go
│ ├── manager_impl_test.go
│ ├── manager_test.go
│ ├── model.go
│ ├── model_impl.go
│ ├── timeline.go
│ └── timeline_impl.go
├── notification
│ ├── handlers.go
│ └── service.go
├── ogp
│ ├── parser
│ │ ├── domain.go
│ │ ├── domain_vrchat.go
│ │ ├── domain_vrchat_test.go
│ │ ├── errors.go
│ │ ├── formatter.go
│ │ ├── parser.go
│ │ └── parser_test.go
│ ├── service.go
│ └── service_impl.go
├── oidc
│ └── userinfo.go
├── qall
│ ├── roomstate.go
│ ├── roomstate_impl.go
│ ├── roomstate_test.go
│ ├── soudboard_impl.go
│ ├── soundboard.go
│ └── soundboard_test.go
├── rbac
│ ├── permission
│ │ ├── bot.go
│ │ ├── channels.go
│ │ ├── client.go
│ │ ├── file.go
│ │ ├── message.go
│ │ ├── notification.go
│ │ ├── permission.go
│ │ ├── stamp.go
│ │ └── user.go
│ ├── rbac.go
│ ├── rbac_impl.go
│ ├── rbac_impl_test.go
│ └── role
│ │ ├── admin.go
│ │ ├── bot.go
│ │ ├── client.go
│ │ ├── manage_bot.go
│ │ ├── openid.go
│ │ ├── profile.go
│ │ ├── read.go
│ │ ├── role.go
│ │ ├── user.go
│ │ └── write.go
├── search
│ ├── engine.go
│ ├── es.go
│ ├── es_result.go
│ ├── es_sync.go
│ └── null.go
├── services.go
├── services_wire.go
├── variable
│ └── variable_type.go
├── viewer
│ ├── manager.go
│ ├── state.go
│ ├── state_with_channel.go
│ ├── state_with_time.go
│ └── user_state.go
├── webrtcv3
│ ├── manager.go
│ └── model.go
└── ws
│ ├── config.go
│ ├── handler.go
│ ├── message.go
│ ├── session.go
│ ├── streamer.go
│ └── target_func.go
├── testdata
├── gif
│ ├── embed.go
│ ├── frog.gif
│ ├── frog_resized.gif
│ ├── miku.gif
│ ├── miku_resized.gif
│ ├── mushroom.gif
│ ├── mushroom_resized.gif
│ ├── new_year.gif
│ ├── new_year_resized.gif
│ ├── surprised.gif
│ ├── surprised_resized.gif
│ ├── tooth.gif
│ └── tooth_resized.gif
└── images
│ ├── embed.go
│ ├── test.png
│ ├── test_fit.png
│ └── test_thumbnail.png
├── testutils
├── empty_test_repository.go
├── gif.go
├── test_rbac.go
└── test_repository.go
├── tools.go
└── utils
├── gormutil
├── error.go
└── util.go
├── gormzap
└── logger.go
├── hmac
├── hmac.go
└── hmac_test.go
├── imaging
├── gif.go
├── gif_test.go
├── icon.go
└── icon_test.go
├── jwt
└── signer.go
├── keymutex.go
├── keymutex_test.go
├── message
├── embedded.go
├── embedded_test.go
├── parser.go
├── parser_test.go
├── replacer.go
├── replacer_test.go
├── spoiler.go
└── spoiler_test.go
├── optional
├── of.go
└── of_test.go
├── private_ip.go
├── private_ip_test.go
├── random
├── ecdsa.go
├── random.go
└── random_test.go
├── secure.go
├── secure_test.go
├── set
├── common.go
├── set.go
├── string.go
└── uuid.go
├── storage
├── composite.go
├── inmemory.go
├── local.go
├── mock_storage
│ └── mock_storage.go
├── s3.go
├── s3_main_test.go
├── s3_object.go
├── s3_object_test.go
├── s3_tester_test.go
├── storage.go
└── swift.go
├── throttle
└── throttlemap.go
├── twemoji
└── installer.go
├── utils.go
└── validator
├── rules.go
├── validate.go
└── validate_test.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | /.circleci/
2 | /.github/
3 | /.idea/
4 | /.git/
5 | /dev/
6 | /docs/
7 | /vendor/
8 | /dist/
9 |
10 | .dockerignore
11 | .envrc
12 | .gitignore
13 | .golangci.yml
14 | .tbls.yml
15 | codecov.yml
16 | config.example.yml
17 | config.yml
18 | compose.yaml
19 | Dockerfile
20 | LICENSE
21 | Makefile
22 | README.md
23 | *.sql
24 |
25 | .DS_Store
26 |
--------------------------------------------------------------------------------
/.github/renovate-bot.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "username": "trap-renovate[bot]",
4 | "gitAuthor": "trap-renovate <138502363+trap-renovate[bot]@users.noreply.github.com>",
5 | "repositories": ["traPtitech/traQ"]
6 | }
7 |
--------------------------------------------------------------------------------
/.github/renovate.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",
5 | "docker:enableMajor",
6 | ":disableRateLimiting",
7 | ":dependencyDashboard",
8 | ":semanticCommits",
9 | "github>traPtitech/traQ//.github/renovate/auto-merge.json5",
10 | "github>traPtitech/traQ//.github/renovate/commit-message.json5",
11 | "github>traPtitech/traQ//.github/renovate/gomod.json5",
12 | "github>traPtitech/traQ//.github/renovate/labels.json5",
13 | "github>traPtitech/traQ//.github/renovate/regex-manager.json5",
14 | "github>traPtitech/traQ//.github/renovate/semantic-commits.json5"
15 | ],
16 | "platform": "github",
17 | "platformAutomerge": true,
18 | "automergeType": "branch",
19 | "rebaseWhen": "conflicted",
20 | "onboarding": false,
21 | "dependencyDashboardTitle": "Renovate Dashboard 🤖",
22 | "suppressNotifications": ["prIgnoreNotification"],
23 | "branchConcurrentLimit": 0,
24 | "prHourlyLimit": 0,
25 | "prConcurrentLimit": 0,
26 | }
27 |
--------------------------------------------------------------------------------
/.github/renovate/auto-merge.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "packageRules": [
4 | {
5 | "description": "Auto merge GitHub Actions",
6 | "matchManagers": ["github-actions"],
7 | "automerge": true,
8 | },
9 | {
10 | "description": "Auto merge up to minor",
11 | "matchUpdateTypes": ["minor", "patch"],
12 | "matchCurrentVersion": "!/^0/",
13 | "automerge": true
14 | },
15 | {
16 | "description": "Auto merge digest",
17 | "matchUpdateTypes": ["digest"],
18 | "automerge": true
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.github/renovate/commit-message.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "commitMessageTopic": "{{depName}}",
4 | "commitMessageExtra": "to {{newVersion}}",
5 | "commitMessageSuffix": "",
6 | "packageRules": [
7 | {
8 | "matchDatasources": ["docker"],
9 | "commitMessageTopic": "image {{depName}}"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.github/renovate/gomod.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "postUpdateOptions": ["gomodTidy", "gomodMassage"]
4 | }
5 |
--------------------------------------------------------------------------------
/.github/renovate/labels.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "packageRules": [
4 | {
5 | "matchUpdateTypes": ["major"],
6 | "labels": ["type/major"]
7 | },
8 | {
9 | "matchUpdateTypes": ["minor"],
10 | "labels": ["type/minor"]
11 | },
12 | {
13 | "matchUpdateTypes": ["patch"],
14 | "labels": ["type/patch"]
15 | },
16 | {
17 | "matchUpdateTypes": ["digest"],
18 | "labels": ["type/digest"]
19 | },
20 | {
21 | "matchDatasources": ["docker"],
22 | "addLabels": ["renovate/container"]
23 | },
24 | {
25 | "matchManagers": ["gomod"],
26 | "addLabels": ["renovate/gomod"]
27 | },
28 | {
29 | "matchDatasources": ["github-releases", "github-tags"],
30 | "addLabels": ["renovate/github-releases"]
31 | },
32 | {
33 | "matchManagers": ["github-actions"],
34 | "addLabels": ["renovate/github-actions"]
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vagrant
2 | .envrc
3 | /.idea/
4 | /.vscode/
5 | /vendor/
6 | /dev/data/
7 | /dist/
8 |
9 | traQ
10 | traQ.exe
11 | .tool-versions
12 |
13 | docs/dbSchema/schema.json
14 | /*.sql
15 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 |
3 | linters:
4 | enable:
5 | - govet
6 | - errcheck
7 | - staticcheck
8 | - unused
9 | - ineffassign
10 | - revive
11 | settings:
12 | errcheck:
13 | exclude-functions:
14 | - (*go.uber.org/zap.Logger).Sync
15 | exclusions:
16 | generated: lax
17 | warn-unused: true
18 | presets:
19 | - comments
20 | - common-false-positives
21 | - legacy
22 | - std-error-handling
23 | formatters:
24 | enable:
25 | - gofmt
26 | exclusions:
27 | generated: lax
28 | warn-unused: true
29 |
--------------------------------------------------------------------------------
/.hadolint.yaml:
--------------------------------------------------------------------------------
1 | ignored:
2 | - DL3007
3 |
--------------------------------------------------------------------------------
/.spectral.yml:
--------------------------------------------------------------------------------
1 | extends: spectral:oas
2 | rules:
3 | operation-success-response: false
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=$BUILDPLATFORM golang:1.24.4 AS build
2 |
3 | RUN mkdir /storage
4 |
5 | WORKDIR /go/src/github.com/traPtitech/traQ
6 |
7 | COPY ./go.* ./
8 | RUN --mount=type=cache,target=/go/pkg/mod go mod download
9 |
10 | ENV GOCACHE=/tmp/go/cache
11 | ENV CGO_ENABLED=0
12 | ARG TRAQ_VERSION=dev
13 | ARG TRAQ_REVISION=local
14 |
15 | ARG TARGETOS
16 | ARG TARGETARCH
17 | ENV GOOS=$TARGETOS
18 | ENV GOARCH=$TARGETARCH
19 |
20 | COPY . .
21 | RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/tmp/go/cache \
22 | go build -o /traQ -ldflags "-s -w -X main.version=$TRAQ_VERSION -X main.revision=$TRAQ_REVISION"
23 |
24 | FROM gcr.io/distroless/base:latest
25 | WORKDIR /app
26 | EXPOSE 3000
27 |
28 | COPY --from=build /storage/ /app/storage/
29 | VOLUME /app/storage
30 |
31 | COPY --from=build /traQ ./
32 |
33 | HEALTHCHECK CMD ["./traQ", "healthcheck", "||", "exit", "1"]
34 | ENTRYPOINT ["./traQ"]
35 | CMD ["serve"]
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 東京工業大学デジタル創作同好会
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 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | To report a security issue, **do not** open a public issue.
6 | Instead, email us at **info@trap.jp** or fill a form at https://trap.jp/request.
7 |
8 | _Optionally_:
9 | - Include as much detail as possible: steps to reproduce, affected versions, and proof-of-concept code.
10 |
--------------------------------------------------------------------------------
/cmd/conf.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/spf13/cobra"
8 | "gopkg.in/yaml.v3"
9 | )
10 |
11 | // confCommand 設定確認・ベース設定プリントコマンド
12 | func confCommand() *cobra.Command {
13 | return &cobra.Command{
14 | Use: "conf",
15 | Short: "Print loaded config variables",
16 | Run: func(_ *cobra.Command, _ []string) {
17 | bs, err := yaml.Marshal(c)
18 | if err != nil {
19 | log.Fatalf("unable to marshal config to YAML: %v", err)
20 | }
21 | fmt.Print(string(bs))
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/cmd/healthcheck.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/spf13/cobra"
8 | "go.uber.org/zap"
9 | )
10 |
11 | // healthcheckCommand ヘルスチェックコマンド
12 | func healthcheckCommand() *cobra.Command {
13 | return &cobra.Command{
14 | Use: "healthcheck",
15 | Short: "Run healthcheck",
16 | Run: func(_ *cobra.Command, _ []string) {
17 | logger := getCLILogger()
18 | defer logger.Sync()
19 |
20 | resp, err := http.DefaultClient.Get(fmt.Sprintf("http://localhost:%d/api/ping", c.Port))
21 | if err != nil {
22 | logger.Fatal("HTTP Client Error", zap.Error(err))
23 | }
24 | defer resp.Body.Close()
25 | if resp.StatusCode != http.StatusOK {
26 | logger.Fatal("Unexpected status", zap.Int("status", resp.StatusCode))
27 | }
28 | },
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/cmd/migrate.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/traPtitech/traQ/migration"
7 | )
8 |
9 | // migrateCommand データベースマイグレーションコマンド
10 | func migrateCommand() *cobra.Command {
11 | var dropDB bool
12 |
13 | cmd := cobra.Command{
14 | Use: "migrate",
15 | Short: "Execute database schema migration only",
16 | RunE: func(_ *cobra.Command, _ []string) error {
17 | engine, err := c.getDatabase()
18 | if err != nil {
19 | return err
20 | }
21 | db, err := engine.DB()
22 | if err != nil {
23 | return err
24 | }
25 | defer db.Close()
26 | if dropDB {
27 | if err := migration.DropAll(engine); err != nil {
28 | return err
29 | }
30 | }
31 | _, err = migration.Migrate(engine)
32 | return err
33 | },
34 | }
35 |
36 | flags := cmd.Flags()
37 | flags.BoolVar(&dropDB, "reset", false, "whether to truncate database (drop all tables)")
38 |
39 | return &cmd
40 | }
41 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // versionCommand バージョンプリントコマンド
10 | func versionCommand() *cobra.Command {
11 | return &cobra.Command{
12 | Use: "version",
13 | Short: "Print version information",
14 | Run: func(_ *cobra.Command, _ []string) {
15 | fmt.Printf("traQ %s (revision %s)\n", Version, Revision)
16 | },
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
2 | coverage:
3 | status:
4 | project:
5 | default:
6 | target: auto
7 | threshold: 3%
8 | base: auto
9 | patch: off
10 |
--------------------------------------------------------------------------------
/dev/.gitignore:
--------------------------------------------------------------------------------
1 | frontend/
2 |
--------------------------------------------------------------------------------
/dev/Caddyfile:
--------------------------------------------------------------------------------
1 | {
2 | admin off
3 | }
4 |
5 | :80
6 |
7 | log
8 | root * /usr/share/caddy
9 |
10 | handle /api/* {
11 | reverse_proxy backend:3000
12 | }
13 | handle /.well-known/* {
14 | reverse_proxy backend:3000
15 | }
16 |
17 | handle {
18 | file_server
19 | try_files {path} /index.html
20 |
21 | header /sw.js Cache-Control "max-age=0"
22 | }
23 |
--------------------------------------------------------------------------------
/dev/Dockerfile-frontend:
--------------------------------------------------------------------------------
1 | FROM caddy:2.10.0-alpine
2 |
3 | COPY ./Caddyfile /etc/caddy/Caddyfile
4 |
5 | RUN wget -O - https://github.com/traPtitech/traQ_S-UI/releases/download/v3.22.1/dist.tar.gz | tar zxv -C /usr/share/caddy/ --strip-components=2
6 |
--------------------------------------------------------------------------------
/dev/bin/bot_debugger.go:
--------------------------------------------------------------------------------
1 | //go:build tools
2 | // +build tools
3 |
4 | package main
5 |
6 | import (
7 | "flag"
8 | "fmt"
9 | "net/http"
10 | "net/http/httputil"
11 | )
12 |
13 | func main() {
14 | var port int
15 | flag.IntVar(&port, "p", 5555, "listen port")
16 | flag.Parse()
17 | http.HandleFunc("/", botHandler)
18 | http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
19 | }
20 |
21 | func botHandler(w http.ResponseWriter, r *http.Request) {
22 | dump, err := httputil.DumpRequest(r, true)
23 | if err != nil {
24 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
25 | return
26 | }
27 | fmt.Printf("%s\n", dump)
28 |
29 | w.WriteHeader(http.StatusNoContent)
30 | }
31 |
--------------------------------------------------------------------------------
/dev/bin/down-test-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eu
4 |
5 | containername=traq-test-db
6 |
7 | if docker ps --all | grep ${containername} > /dev/null; then
8 | echo "remove ${containername} docker container"
9 | docker rm -f -v ${containername}
10 | fi
11 |
--------------------------------------------------------------------------------
/dev/bin/gen_ec_pem.go:
--------------------------------------------------------------------------------
1 | //go:build tools
2 | // +build tools
3 |
4 | package main
5 |
6 | import (
7 | "crypto/ecdsa"
8 | "crypto/elliptic"
9 | "crypto/rand"
10 | "crypto/x509"
11 | "encoding/pem"
12 | "log"
13 | "os"
14 | )
15 |
16 | func main() {
17 | priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
18 | ecder, _ := x509.MarshalECPrivateKey(priv)
19 | ecderpub, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey)
20 |
21 | pripem, err := os.OpenFile("ec.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
22 | if err != nil {
23 | log.Fatal(err)
24 | }
25 | pem.Encode(pripem, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ecder})
26 |
27 | pubpem, err := os.OpenFile("ec_pub.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
28 | if err != nil {
29 | log.Fatal(err)
30 | }
31 | pem.Encode(pubpem, &pem.Block{Type: "PUBLIC KEY", Bytes: ecderpub})
32 | }
33 |
--------------------------------------------------------------------------------
/dev/bin/up-test-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eu
4 |
5 | containername=traq-test-db
6 | port=${TEST_DB_PORT:-3100}
7 |
8 | if docker ps | grep ${containername} > /dev/null; then
9 | exit 0 # 既にテストDBコンテナが起動している
10 | fi
11 |
12 | if docker ps --all | grep ${containername} > /dev/null; then
13 | echo "restart ${containername} docker container"
14 | docker restart ${containername}
15 | else
16 | echo "create ${containername} docker container"
17 | docker run --name ${containername} -p ${port}:3306 -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=traq -d mariadb:10.6.4 \
18 | mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
19 | fi
20 |
21 |
--------------------------------------------------------------------------------
/dev/es_jvm.options:
--------------------------------------------------------------------------------
1 | -Xms256m
2 | -Xmx256m
3 |
--------------------------------------------------------------------------------
/docs/dbSchema/bot_join_channels.md:
--------------------------------------------------------------------------------
1 | # bot_join_channels
2 |
3 | ## Description
4 |
5 | BOT参加チャンネルテーブル
6 |
7 |
8 | Table Definition
9 |
10 | ```sql
11 | CREATE TABLE `bot_join_channels` (
12 | `channel_id` char(36) NOT NULL,
13 | `bot_id` char(36) NOT NULL,
14 | PRIMARY KEY (`channel_id`,`bot_id`)
15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
16 | ```
17 |
18 |
19 |
20 | ## Columns
21 |
22 | | Name | Type | Default | Nullable | Children | Parents | Comment |
23 | | ---- | ---- | ------- | -------- | -------- | ------- | ------- |
24 | | channel_id | char(36) | | false | | | チャンネルUUID |
25 | | bot_id | char(36) | | false | | | BOT UUID |
26 |
27 | ## Constraints
28 |
29 | | Name | Type | Definition |
30 | | ---- | ---- | ---------- |
31 | | PRIMARY | PRIMARY KEY | PRIMARY KEY (channel_id, bot_id) |
32 |
33 | ## Indexes
34 |
35 | | Name | Definition |
36 | | ---- | ---------- |
37 | | PRIMARY | PRIMARY KEY (channel_id, bot_id) USING BTREE |
38 |
39 | ## Relations
40 |
41 | ```mermaid
42 | erDiagram
43 |
44 |
45 | "bot_join_channels" {
46 | char_36_ channel_id PK
47 | char_36_ bot_id PK
48 | }
49 | ```
50 |
51 | ---
52 |
53 | > Generated by [tbls](https://github.com/k1LoW/tbls)
54 |
--------------------------------------------------------------------------------
/docs/dbSchema/channel_latest_messages.md:
--------------------------------------------------------------------------------
1 | # channel_latest_messages
2 |
3 | ## Description
4 |
5 | チャンネル最新メッセージテーブル
6 |
7 |
8 | Table Definition
9 |
10 | ```sql
11 | CREATE TABLE `channel_latest_messages` (
12 | `channel_id` char(36) NOT NULL,
13 | `message_id` char(36) NOT NULL,
14 | `date_time` datetime(6) DEFAULT NULL,
15 | PRIMARY KEY (`channel_id`),
16 | KEY `idx_channel_latest_messages_date_time` (`date_time`)
17 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
18 | ```
19 |
20 |
21 |
22 | ## Columns
23 |
24 | | Name | Type | Default | Nullable | Children | Parents | Comment |
25 | | ---- | ---- | ------- | -------- | -------- | ------- | ------- |
26 | | channel_id | char(36) | | false | | | チャンネルUUID |
27 | | message_id | char(36) | | false | | | メッセージUUID |
28 | | date_time | datetime(6) | NULL | true | | | メッセージ作成日時 |
29 |
30 | ## Constraints
31 |
32 | | Name | Type | Definition |
33 | | ---- | ---- | ---------- |
34 | | PRIMARY | PRIMARY KEY | PRIMARY KEY (channel_id) |
35 |
36 | ## Indexes
37 |
38 | | Name | Definition |
39 | | ---- | ---------- |
40 | | idx_channel_latest_messages_date_time | KEY idx_channel_latest_messages_date_time (date_time) USING BTREE |
41 | | PRIMARY | PRIMARY KEY (channel_id) USING BTREE |
42 |
43 | ## Relations
44 |
45 | ```mermaid
46 | erDiagram
47 |
48 |
49 | "channel_latest_messages" {
50 | char_36_ channel_id PK
51 | char_36_ message_id
52 | datetime_6_ date_time
53 | }
54 | ```
55 |
56 | ---
57 |
58 | > Generated by [tbls](https://github.com/k1LoW/tbls)
59 |
--------------------------------------------------------------------------------
/docs/dbSchema/migrations.md:
--------------------------------------------------------------------------------
1 | # migrations
2 |
3 | ## Description
4 |
5 | gormigrate用のデータベースバージョンテーブル
6 |
7 |
8 | Table Definition
9 |
10 | ```sql
11 | CREATE TABLE `migrations` (
12 | `id` varchar(190) NOT NULL,
13 | PRIMARY KEY (`id`)
14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
15 | ```
16 |
17 |
18 |
19 | ## Columns
20 |
21 | | Name | Type | Default | Nullable | Children | Parents | Comment |
22 | | ---- | ---- | ------- | -------- | -------- | ------- | ------- |
23 | | id | varchar(190) | | false | | | |
24 |
25 | ## Constraints
26 |
27 | | Name | Type | Definition |
28 | | ---- | ---- | ---------- |
29 | | PRIMARY | PRIMARY KEY | PRIMARY KEY (id) |
30 |
31 | ## Indexes
32 |
33 | | Name | Definition |
34 | | ---- | ---------- |
35 | | PRIMARY | PRIMARY KEY (id) USING BTREE |
36 |
37 | ## Relations
38 |
39 | ```mermaid
40 | erDiagram
41 |
42 |
43 | "migrations" {
44 | varchar_190_ id PK
45 | }
46 | ```
47 |
48 | ---
49 |
50 | > Generated by [tbls](https://github.com/k1LoW/tbls)
51 |
--------------------------------------------------------------------------------
/docs/dbSchema/soundboard_items.md:
--------------------------------------------------------------------------------
1 | # soundboard_items
2 |
3 | ## Description
4 |
5 | サウンドボードアイテムテーブル
6 |
7 |
8 | Table Definition
9 |
10 | ```sql
11 | CREATE TABLE `soundboard_items` (
12 | `id` char(36) NOT NULL,
13 | `name` varchar(32) NOT NULL,
14 | `stamp_id` char(36) DEFAULT NULL,
15 | `creator_id` char(36) NOT NULL,
16 | PRIMARY KEY (`id`)
17 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
18 | ```
19 |
20 |
21 |
22 | ## Columns
23 |
24 | | Name | Type | Default | Nullable | Children | Parents | Comment |
25 | | ---- | ---- | ------- | -------- | -------- | ------- | ------- |
26 | | id | char(36) | | false | | | |
27 | | name | varchar(32) | | false | | | アイテム名 |
28 | | stamp_id | char(36) | NULL | true | | | スタンプUUID |
29 | | creator_id | char(36) | | false | | | アイテム作成者UUID |
30 |
31 | ## Constraints
32 |
33 | | Name | Type | Definition |
34 | | ---- | ---- | ---------- |
35 | | PRIMARY | PRIMARY KEY | PRIMARY KEY (id) |
36 |
37 | ## Indexes
38 |
39 | | Name | Definition |
40 | | ---- | ---------- |
41 | | PRIMARY | PRIMARY KEY (id) USING BTREE |
42 |
43 | ## Relations
44 |
45 | ```mermaid
46 | erDiagram
47 |
48 |
49 | "soundboard_items" {
50 | char_36_ id PK
51 | varchar_32_ name
52 | char_36_ stamp_id
53 | char_36_ creator_id
54 | }
55 | ```
56 |
57 | ---
58 |
59 | > Generated by [tbls](https://github.com/k1LoW/tbls)
60 |
--------------------------------------------------------------------------------
/docs/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/docs/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/docs/favicon-32x32.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/traPtitech/traQ/cmd"
7 | )
8 |
9 | var (
10 | version = "UNKNOWN"
11 | revision = "UNKNOWN"
12 | )
13 |
14 | func main() {
15 | cmd.Version = version
16 | cmd.Revision = revision
17 | if err := cmd.Execute(); err != nil {
18 | log.Fatal(err)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/migration/doc.go:
--------------------------------------------------------------------------------
1 | // Package migration では、gopkg.in/gormigrate.v2を用いたデータベースマイグレーションコードを記述します。
2 | // データベースに新たなテーブルの追加や、既にあるテーブルのカラムの型の変更などのスキーマの改変を行う場合、必ずその改変処理(SQLクエリ)を全てここに記述してください。
3 | // サーバー起動時に自動的にマイグレーションが実行されます。
4 | //
5 | // # Instruction 記述方法
6 | //
7 | // 既にv**.goに記述されているように、IDをインクリメントして*gormigrate.Migrationを返す関数を生成し、current.goにあるMigrationsに実装したマイグレーションを追加してください。
8 | // 更に、全てのマイグレーションを適用後の最新のデータベーススキーマに関する情報を、AllTablesと対応する構造体のタグを変更することによって記述してください。
9 | //
10 | // v6.goの様に、新たに作成するテーブルや、古いバージョンのテーブルの情報を新しいテーブルに移行させる時に使う構造体は、modelパッケージに定義する・されている構造体を使うのではなく、
11 | // v6UserGroupAdmin、v6OldUserGroupなどの様に、マイグレーションコード内に再定義したものを使ってください。
12 | package migration
13 |
--------------------------------------------------------------------------------
/migration/migrate.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 |
7 | "github.com/go-gormigrate/gormigrate/v2"
8 | "gorm.io/gorm"
9 | )
10 |
11 | // Migrate データベースマイグレーションを実行します
12 | // 初回実行でスキーマが初期化された場合、initでtrueを返します
13 | func Migrate(db *gorm.DB) (init bool, err error) {
14 | m := gormigrate.New(db, &gormigrate.Options{
15 | TableName: "migrations",
16 | IDColumnName: "id",
17 | IDColumnSize: 190,
18 | UseTransaction: false,
19 | ValidateUnknownMigrations: true,
20 | }, Migrations())
21 | m.InitSchema(func(db *gorm.DB) error {
22 | // 初回のみに呼ばれる
23 | // 全ての最新のデータベース定義を書く事
24 | init = true
25 |
26 | // テーブル
27 | return db.AutoMigrate(AllTables()...)
28 | })
29 | err = m.Migrate()
30 | return
31 | }
32 |
33 | // DropAll データベースの全テーブルを削除します
34 | func DropAll(db *gorm.DB) error {
35 | if err := db.Migrator().DropTable(AllTables()...); err != nil {
36 | return err
37 | }
38 | return db.Migrator().DropTable("migrations")
39 | }
40 |
41 | // CreateDatabasesIfNotExists データベースが存在しなければ作成します
42 | func CreateDatabasesIfNotExists(dialect, dsn, prefix string, names ...string) error {
43 | conn, err := sql.Open(dialect, dsn)
44 | if err != nil {
45 | return err
46 | }
47 | defer conn.Close()
48 | for _, v := range names {
49 | _, err = conn.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s%s`", prefix, v))
50 | if err != nil {
51 | return err
52 | }
53 | }
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/migration/v1.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // v1 インデックスidx_messages_deleted_atの削除とidx_messages_channel_id_deleted_at_created_atの追加
9 | func v1() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "1",
12 | Migrate: func(db *gorm.DB) error {
13 | if err := db.Migrator().DropIndex("messages", "idx_messages_deleted_at"); err != nil {
14 | return err
15 | }
16 | return db.Exec("ALTER TABLE `messages` ADD KEY `idx_messages_channel_id_deleted_at_created_at` (`channel_id`, `deleted_at`, `created_at`)").Error
17 | },
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/migration/v12.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/go-gormigrate/gormigrate/v2"
8 | "github.com/gofrs/uuid"
9 | "gorm.io/gorm"
10 |
11 | "github.com/traPtitech/traQ/model"
12 | )
13 |
14 | // v12 カスタムスタンプパレット機能追加
15 | func v12() *gormigrate.Migration {
16 | return &gormigrate.Migration{
17 | ID: "12",
18 | Migrate: func(db *gorm.DB) error {
19 | if err := db.AutoMigrate(&v12StampPalette{}); err != nil {
20 | return err
21 | }
22 |
23 | foreignKeys := [][6]string{
24 | // table name, constraint name, field name, references, on delete, on update
25 | {"stamp_palettes", "stamp_palettes_creator_id_users_id_foreign", "creator_id", "users(id)", "CASCADE", "CASCADE"},
26 | }
27 |
28 | for _, c := range foreignKeys {
29 | if err := db.Exec(fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s ON DELETE %s ON UPDATE %s", c[0], c[1], c[2], c[3], c[4], c[5])).Error; err != nil {
30 | return err
31 | }
32 | }
33 |
34 | return nil
35 | },
36 | }
37 | }
38 |
39 | type v12StampPalette struct {
40 | ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
41 | Name string `gorm:"type:varchar(30);not null"`
42 | Description string `gorm:"type:text;not null"`
43 | Stamps model.UUIDs `gorm:"type:text;not null"`
44 | CreatorID uuid.UUID `gorm:"type:char(36);not null;index"`
45 | CreatedAt time.Time `gorm:"precision:6"`
46 | UpdatedAt time.Time `gorm:"precision:6"`
47 | }
48 |
49 | func (*v12StampPalette) TableName() string {
50 | return "stamp_palettes"
51 | }
52 |
--------------------------------------------------------------------------------
/migration/v13.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // v13 パーミッション調整・インデックス付与
11 | func v13() *gormigrate.Migration {
12 | return &gormigrate.Migration{
13 | ID: "13",
14 | Migrate: func(db *gorm.DB) error {
15 | addedRolePermissions := map[string][]string{
16 | "bot": {
17 | "delete_file",
18 | },
19 | "write": {
20 | "delete_file",
21 | },
22 | }
23 | for role, perms := range addedRolePermissions {
24 | for _, perm := range perms {
25 | if err := db.Create(&v13RolePermission{Role: role, Permission: perm}).Error; err != nil {
26 | return err
27 | }
28 | }
29 | }
30 |
31 | indexes := [][3]string{
32 | // table name, index name, field names
33 | {"files", "idx_files_creator_id_created_at", "(creator_id, created_at)"},
34 | {"messages_stamps", "idx_messages_stamps_user_id_stamp_id_updated_at", "(user_id, stamp_id, updated_at)"},
35 | }
36 | for _, c := range indexes {
37 | if err := db.Exec(fmt.Sprintf("ALTER TABLE %s ADD KEY %s %s", c[0], c[1], c[2])).Error; err != nil {
38 | return err
39 | }
40 | }
41 | return nil
42 | },
43 | }
44 | }
45 |
46 | type v13RolePermission struct {
47 | Role string `gorm:"type:varchar(30);not null;primaryKey"`
48 | Permission string `gorm:"type:varchar(30);not null;primaryKey"`
49 | }
50 |
51 | func (*v13RolePermission) TableName() string {
52 | return "user_role_permissions"
53 | }
54 |
--------------------------------------------------------------------------------
/migration/v14.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // v14 パーミッション不足修正
9 | func v14() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "14",
12 | Migrate: func(db *gorm.DB) error {
13 | addedRolePermissions := map[string][]string{
14 | "user": {
15 | "get_clip_folder",
16 | "get_stamp_palette",
17 | "create_clip_folder",
18 | "edit_clip_folder",
19 | "delete_clip_folder",
20 | "create_stamp_palette",
21 | "edit_stamp_palette",
22 | "delete_stamp_palette",
23 | "delete_file",
24 | },
25 | }
26 | for role, perms := range addedRolePermissions {
27 | for _, perm := range perms {
28 | if err := db.Create(&v14RolePermission{Role: role, Permission: perm}).Error; err != nil {
29 | return err
30 | }
31 | }
32 | }
33 | return nil
34 | },
35 | }
36 | }
37 |
38 | type v14RolePermission struct {
39 | Role string `gorm:"type:varchar(30);not null;primaryKey"`
40 | Permission string `gorm:"type:varchar(30);not null;primaryKey"`
41 | }
42 |
43 | func (*v14RolePermission) TableName() string {
44 | return "user_role_permissions"
45 | }
46 |
--------------------------------------------------------------------------------
/migration/v16.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // v16 パーミッション修正
9 | func v16() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "16",
12 | Migrate: func(db *gorm.DB) error {
13 | addedRolePermissions := map[string][]string{
14 | "user": {
15 | "get_my_external_account",
16 | "edit_my_external_account",
17 | },
18 | }
19 | for role, perms := range addedRolePermissions {
20 | for _, perm := range perms {
21 | if err := db.Create(&v16RolePermission{Role: role, Permission: perm}).Error; err != nil {
22 | return err
23 | }
24 | }
25 | }
26 | return nil
27 | },
28 | }
29 | }
30 |
31 | type v16RolePermission struct {
32 | Role string `gorm:"type:varchar(30);not null;primaryKey"`
33 | Permission string `gorm:"type:varchar(30);not null;primaryKey"`
34 | }
35 |
36 | func (*v16RolePermission) TableName() string {
37 | return "user_role_permissions"
38 | }
39 |
--------------------------------------------------------------------------------
/migration/v18.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // v18 インデックス追加
11 | func v18() *gormigrate.Migration {
12 | return &gormigrate.Migration{
13 | ID: "18",
14 | Migrate: func(db *gorm.DB) error {
15 | // 複合インデックス
16 | indexes := [][3]string{
17 | // table name, index name, field names
18 | {"channels", "idx_channels_channels_id_is_public_is_forced", "(id, is_public, is_forced)"},
19 | {"messages", "idx_messages_deleted_at_created_at", "(deleted_at, created_at)"},
20 | }
21 | for _, c := range indexes {
22 | if err := db.Exec(fmt.Sprintf("ALTER TABLE %s ADD KEY %s %s", c[0], c[1], c[2])).Error; err != nil {
23 | return err
24 | }
25 | }
26 | return nil
27 | },
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/migration/v19.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "github.com/gofrs/uuid"
8 | "gorm.io/gorm"
9 | )
10 |
11 | // v19 httpセッション管理テーブル変更
12 | func v19() *gormigrate.Migration {
13 | return &gormigrate.Migration{
14 | ID: "19",
15 | Migrate: func(db *gorm.DB) error {
16 | if err := db.Migrator().DropColumn(v19OldSessionRecord{}, "last_access"); err != nil {
17 | return err
18 | }
19 | if err := db.Migrator().DropColumn(v19OldSessionRecord{}, "last_ip"); err != nil {
20 | return err
21 | }
22 | return db.Migrator().DropColumn(v19OldSessionRecord{}, "last_user_agent")
23 | },
24 | }
25 | }
26 |
27 | type v19OldSessionRecord struct {
28 | Token string `gorm:"type:varchar(50);primaryKey"`
29 | ReferenceID uuid.UUID `gorm:"type:char(36);unique"`
30 | UserID uuid.UUID `gorm:"type:varchar(36);index"`
31 | LastAccess time.Time `gorm:"precision:6"`
32 | LastIP string `gorm:"type:text"`
33 | LastUserAgent string `gorm:"type:text"`
34 | Data []byte `gorm:"type:longblob"`
35 | Created time.Time `gorm:"precision:6"`
36 | }
37 |
38 | func (v19OldSessionRecord) TableName() string {
39 | return "r_sessions"
40 | }
41 |
--------------------------------------------------------------------------------
/migration/v20.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // v20 パーミッション周りの調整
9 | func v20() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "20",
12 | Migrate: func(db *gorm.DB) error {
13 | deletedPermissions := []string{
14 | "get_heartbeat",
15 | "post_heartbeat",
16 | }
17 | for _, v := range deletedPermissions {
18 | if err := db.Delete(v20RolePermission{}, v20RolePermission{Permission: v}).Error; err != nil {
19 | return err
20 | }
21 | }
22 | return nil
23 | },
24 | }
25 | }
26 |
27 | type v20RolePermission struct {
28 | Role string `gorm:"type:varchar(30);not null;primaryKey"`
29 | Permission string `gorm:"type:varchar(30);not null;primaryKey"`
30 | }
31 |
32 | func (*v20RolePermission) TableName() string {
33 | return "user_role_permissions"
34 | }
35 |
--------------------------------------------------------------------------------
/migration/v21.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | )
11 |
12 | // v21 OGPキャッシュ追加
13 | func v21() *gormigrate.Migration {
14 | return &gormigrate.Migration{
15 | ID: "21",
16 | Migrate: func(db *gorm.DB) error {
17 | return db.AutoMigrate(&v21OgpCache{})
18 | },
19 | }
20 | }
21 |
22 | type v21OgpCache struct {
23 | ID int `gorm:"auto_increment;not null;primaryKey"`
24 | URL string `gorm:"type:text;not null"`
25 | URLHash string `gorm:"type:char(40);not null;index"`
26 | Valid bool `gorm:"type:boolean"`
27 | Content model.Ogp `gorm:"type:text"`
28 | ExpiresAt time.Time `gorm:"precision:6"`
29 | }
30 |
31 | func (ogp v21OgpCache) TableName() string {
32 | return "ogp_cache"
33 | }
34 |
--------------------------------------------------------------------------------
/migration/v22.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // v22 BOTへのWebRTCパーミッションの付与
9 | func v22() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "22",
12 | Migrate: func(db *gorm.DB) error {
13 | addedRolePermissions := map[string][]string{
14 | "bot": {
15 | "web_rtc",
16 | },
17 | }
18 | for role, perms := range addedRolePermissions {
19 | for _, perm := range perms {
20 | if err := db.Create(&v22RolePermission{Role: role, Permission: perm}).Error; err != nil {
21 | return err
22 | }
23 | }
24 | }
25 | return nil
26 | },
27 | }
28 | }
29 |
30 | type v22RolePermission struct {
31 | Role string `gorm:"type:varchar(30);not null;primaryKey"`
32 | Permission string `gorm:"type:varchar(30);not null;primaryKey"`
33 | }
34 |
35 | func (*v22RolePermission) TableName() string {
36 | return "user_role_permissions"
37 | }
38 |
--------------------------------------------------------------------------------
/migration/v23.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // v23 複合インデックス追加
11 | func v23() *gormigrate.Migration {
12 | return &gormigrate.Migration{
13 | ID: "23",
14 | Migrate: func(db *gorm.DB) error {
15 | // 複合インデックス
16 | indexes := [][3]string{
17 | // table name, index name, field names
18 | {"messages", "idx_messages_deleted_at_updated_at", "(deleted_at, updated_at)"},
19 | }
20 | for _, c := range indexes {
21 | if err := db.Exec(fmt.Sprintf("ALTER TABLE %s ADD KEY %s %s", c[0], c[1], c[2])).Error; err != nil {
22 | return err
23 | }
24 | }
25 | return nil
26 | },
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/migration/v24.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "github.com/gofrs/uuid"
8 | "gorm.io/gorm"
9 | )
10 |
11 | // v24 ユーザー設定追加
12 | func v24() *gormigrate.Migration {
13 | return &gormigrate.Migration{
14 | ID: "24",
15 | Migrate: func(db *gorm.DB) error {
16 | // ユーザー設定追加
17 | if err := db.AutoMigrate(&v24UserSettings{}); err != nil {
18 | return err
19 | }
20 |
21 | foreignKeys := [][6]string{
22 | // table name, constraint name, field name, references, on delete, on update
23 | {"user_settings", "user_settings_user_id_users_id_foreign", "user_id", "users(id)", "CASCADE", "CASCADE"},
24 | }
25 | for _, c := range foreignKeys {
26 | if err := db.Exec(fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s ON DELETE %s ON UPDATE %s", c[0], c[1], c[2], c[3], c[4], c[5])).Error; err != nil {
27 | return err
28 | }
29 | }
30 | return nil
31 | },
32 | }
33 | }
34 |
35 | type v24UserSettings struct {
36 | UserID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
37 | NotifyCitation bool `gorm:"type:boolean;not null;default:false"`
38 | }
39 |
40 | func (*v24UserSettings) TableName() string {
41 | return "user_settings"
42 | }
43 |
--------------------------------------------------------------------------------
/migration/v30.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "github.com/gofrs/uuid"
8 | "gorm.io/gorm"
9 | )
10 |
11 | // v30 bot_event_logsにresultを追加
12 | func v30() *gormigrate.Migration {
13 | return &gormigrate.Migration{
14 | ID: "30",
15 | Migrate: func(db *gorm.DB) error {
16 | if err := db.AutoMigrate(&v30BotEventLog{}); err != nil {
17 | return err
18 | }
19 | return db.Exec("UPDATE bot_event_logs SET result = CASE WHEN code = 204 THEN 'ok' WHEN code = -1 THEN 'ne' ELSE 'ng' END").Error
20 | },
21 | }
22 | }
23 |
24 | type v30BotEventLog struct {
25 | RequestID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
26 | BotID uuid.UUID `gorm:"type:char(36);not null;index:bot_id_date_time_idx"`
27 | Event v30BotEventType `gorm:"type:varchar(30);not null"`
28 | Body string `gorm:"type:text"`
29 | Result string `gorm:"type:char(2);not null"`
30 | Error string `gorm:"type:text"`
31 | Code int `gorm:"not null;default:0"`
32 | Latency int64 `gorm:"not null;default:0"`
33 | DateTime time.Time `gorm:"precision:6;index:bot_id_date_time_idx"`
34 | }
35 |
36 | func (*v30BotEventLog) TableName() string {
37 | return "bot_event_logs"
38 | }
39 |
40 | type v30BotEventType string
41 |
--------------------------------------------------------------------------------
/migration/v31.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // v31 お気に入りスタンプパーミッション削除(削除忘れ)
9 | func v31() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "31",
12 | Migrate: func(db *gorm.DB) error {
13 | removedPermissions := []string{
14 | "get_favorite_stamp",
15 | "edit_favorite_stamp",
16 | }
17 | for _, perm := range removedPermissions {
18 | if err := db.Delete(&v31RolePermission{}, &v31RolePermission{Permission: perm}).Error; err != nil {
19 | return err
20 | }
21 | }
22 | return nil
23 | },
24 | }
25 | }
26 |
27 | // v31RolePermission ロール権限構造体
28 | type v31RolePermission struct {
29 | Role string `gorm:"type:varchar(30);not null;primaryKey"`
30 | Permission string `gorm:"type:varchar(30);not null;primaryKey"`
31 | }
32 |
33 | // TableName RolePermission構造体のテーブル名
34 | func (*v31RolePermission) TableName() string {
35 | return "user_role_permissions"
36 | }
37 |
--------------------------------------------------------------------------------
/migration/v32.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "github.com/gofrs/uuid"
8 | "gorm.io/gorm"
9 | )
10 |
11 | // ユーザーの表示名上限を32文字に
12 | func v32() *gormigrate.Migration {
13 | return &gormigrate.Migration{
14 | ID: "32",
15 | Migrate: func(db *gorm.DB) error {
16 | if err := db.Exec("UPDATE `users` SET `display_name` = LEFT(`display_name`, 32) WHERE CHAR_LENGTH(`display_name`) > 32").Error; err != nil {
17 | return err
18 | }
19 | return db.AutoMigrate(&v32User{})
20 | },
21 | }
22 | }
23 |
24 | // v32User ユーザー構造体
25 | type v32User struct {
26 | ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
27 | Name string `gorm:"type:varchar(32);not null;unique"`
28 | DisplayName string `gorm:"type:varchar(32);not null;default:''"`
29 | Password string `gorm:"type:char(128);not null;default:''"`
30 | Salt string `gorm:"type:char(128);not null;default:''"`
31 | Icon uuid.UUID `gorm:"type:char(36);not null"`
32 | Status v32UserAccountStatus `gorm:"type:tinyint;not null;default:0"`
33 | Bot bool `gorm:"type:boolean;not null;default:false"`
34 | Role string `gorm:"type:varchar(30);not null;default:'user'"`
35 | CreatedAt time.Time `gorm:"precision:6"`
36 | UpdatedAt time.Time `gorm:"precision:6"`
37 | }
38 |
39 | type (
40 | v32UserAccountStatus int
41 | )
42 |
43 | // TableName User構造体のテーブル名
44 | func (*v32User) TableName() string {
45 | return "users"
46 | }
47 |
--------------------------------------------------------------------------------
/migration/v34.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "github.com/gofrs/uuid"
8 | "gorm.io/gorm"
9 | )
10 |
11 | // v34 未読テーブルのcreated_atカラムをメッセージテーブルを元に更新 / カラム名を変更
12 | func v34() *gormigrate.Migration {
13 | return &gormigrate.Migration{
14 | ID: "34",
15 | Migrate: func(db *gorm.DB) error {
16 | // カラム名を変更
17 | if err := db.Exec("ALTER TABLE unreads CHANGE COLUMN created_at message_created_at DATETIME(6)").Error; err != nil {
18 | return err
19 | }
20 |
21 | // 未読テーブルのmessage_created_atを該当メッセージのcreated_atに更新
22 | if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&v34Unread{}).Updates(map[string]any{
23 | "message_created_at": db.Table("messages").Where("messages.id = unreads.message_id").Select("created_at"),
24 | }).Error; err != nil {
25 | return err
26 | }
27 |
28 | // 削除されたメッセージの未読を削除
29 | return db.Delete(&v34Unread{}, "message_created_at IS NULL").Error
30 | },
31 | }
32 | }
33 |
34 | // v34Unread 未読レコード構造体
35 | type v34Unread struct {
36 | UserID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
37 | ChannelID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
38 | MessageID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
39 | Noticeable bool `gorm:"type:boolean;not null;default:false"`
40 | MessageCreatedAt time.Time `gorm:"precision:6"`
41 | }
42 |
43 | // TableName v34Unread構造体のテーブル名
44 | func (*v34Unread) TableName() string {
45 | return "unreads"
46 | }
47 |
--------------------------------------------------------------------------------
/migration/v36.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // v36 delete_my_stampパーミッションを追加
9 | func v36() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "36",
12 | Migrate: func(db *gorm.DB) error {
13 | addedRolePermissions := map[string][]string{
14 | "user": {
15 | "delete_my_stamp",
16 | },
17 | "write": {
18 | "delete_my_stamp",
19 | },
20 | }
21 | for role, perms := range addedRolePermissions {
22 | for _, perm := range perms {
23 | if err := db.Create(&v36RolePermission{Role: role, Permission: perm}).Error; err != nil {
24 | return err
25 | }
26 | }
27 | }
28 | return nil
29 | },
30 | }
31 | }
32 |
33 | type v36RolePermission struct {
34 | Role string `gorm:"type:varchar(30);not null;primaryKey"`
35 | Permission string `gorm:"type:varchar(30);not null;primaryKey"`
36 | }
37 |
38 | func (*v36RolePermission) TableName() string {
39 | return "user_role_permissions"
40 | }
41 |
--------------------------------------------------------------------------------
/migration/v37.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "github.com/gofrs/uuid"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // v37 サウンドボードアイテム追加
10 | func v37() *gormigrate.Migration {
11 | return &gormigrate.Migration{
12 | ID: "37",
13 | Migrate: func(db *gorm.DB) error {
14 | return db.AutoMigrate(&v37SoundboardItem{})
15 | },
16 | }
17 | }
18 |
19 | type v37SoundboardItem struct {
20 | ID uuid.UUID `gorm:"type:char(36);not null;primary_key" json:"id"`
21 | Name string `gorm:"type:varchar(32);not null" json:"name"`
22 | StampID *uuid.UUID `gorm:"type:char(36)" json:"stampId"`
23 | CreatorID uuid.UUID `gorm:"type:char(36);not null" json:"creatorId"`
24 | }
25 |
--------------------------------------------------------------------------------
/migration/v38.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // v38 v37で作ったサウンドボードアイテムのテーブル名変更
9 | func v38() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "38",
12 | Migrate: func(db *gorm.DB) error {
13 | return db.Migrator().RenameTable(&v37SoundboardItem{}, "soundboard_items")
14 | },
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/migration/v4.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-gormigrate/gormigrate/v2"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // v4 Webhook, Bot外部キー
11 | func v4() *gormigrate.Migration {
12 | return &gormigrate.Migration{
13 | ID: "4",
14 | Migrate: func(db *gorm.DB) error {
15 | foreignKeys := [][6]string{
16 | // table name, constraint name, field name, references, on delete, on update
17 | {"webhook_bots", "webhook_bots_creator_id_users_id_foreign", "creator_id", "users(id)", "CASCADE", "CASCADE"},
18 | {"webhook_bots", "webhook_bots_channel_id_channels_id_foreign", "channel_id", "channels(id)", "CASCADE", "CASCADE"},
19 | {"bots", "bots_creator_id_users_id_foreign", "creator_id", "users(id)", "CASCADE", "CASCADE"},
20 | {"bots", "bots_bot_user_id_users_id_foreign", "bot_user_id", "users(id)", "CASCADE", "CASCADE"},
21 | }
22 | for _, c := range foreignKeys {
23 | if err := db.Exec(fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s ON DELETE %s ON UPDATE %s", c[0], c[1], c[2], c[3], c[4], c[5])).Error; err != nil {
24 | return err
25 | }
26 | }
27 | return nil
28 | },
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/migration/v8.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "github.com/gofrs/uuid"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // v8 チャンネル購読拡張
10 | func v8() *gormigrate.Migration {
11 | return &gormigrate.Migration{
12 | ID: "8",
13 | Migrate: func(db *gorm.DB) error {
14 | if err := db.AutoMigrate(&v8UserSubscribeChannel{}); err != nil {
15 | return err
16 | }
17 |
18 | return db.Table(v8UserSubscribeChannel{}.TableName()).Updates(map[string]interface{}{"mark": true, "notify": true}).Error
19 | },
20 | }
21 | }
22 |
23 | type v8UserSubscribeChannel struct {
24 | UserID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
25 | ChannelID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
26 | Mark bool `gorm:"type:boolean;not null;default:false"` // 追加
27 | Notify bool `gorm:"type:boolean;not null;default:false"` // 追加
28 | }
29 |
30 | func (v8UserSubscribeChannel) TableName() string {
31 | return "users_subscribe_channels"
32 | }
33 |
--------------------------------------------------------------------------------
/model/clip.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | )
8 |
9 | // ClipFolder クリップフォルダーの構造体
10 | type ClipFolder struct {
11 | ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
12 | Name string `gorm:"type:varchar(30);not null"`
13 | Description string `gorm:"type:text;not null"`
14 | OwnerID uuid.UUID `gorm:"type:char(36);not null;index"`
15 | CreatedAt time.Time `gorm:"precision:6"`
16 |
17 | Owner *User `gorm:"constraint:clip_folders_owner_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:OwnerID"`
18 | }
19 |
20 | // TableName ClipFolder構造体のテーブル名
21 | func (*ClipFolder) TableName() string {
22 | return "clip_folders"
23 | }
24 |
25 | // ClipFolderMessage クリップフォルダーのメッセージの構造体
26 | type ClipFolderMessage struct {
27 | FolderID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
28 | MessageID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
29 | CreatedAt time.Time `gorm:"precision:6"`
30 |
31 | Folder ClipFolder `gorm:"constraint:clip_folder_messages_folder_id_clip_folders_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:FolderID"`
32 | Message Message `gorm:"constraint:clip_folder_messages_message_id_messages_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE"`
33 | }
34 |
35 | // TableName ClipFolderMessage構造体のテーブル名
36 | func (*ClipFolderMessage) TableName() string {
37 | return "clip_folder_messages"
38 | }
39 |
--------------------------------------------------------------------------------
/model/devices.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | )
8 |
9 | // Device 通知デバイスの構造体
10 | type Device struct {
11 | Token string `gorm:"type:varchar(190);not null;primaryKey"`
12 | UserID uuid.UUID `gorm:"type:char(36);not null;index"`
13 | CreatedAt time.Time `gorm:"precision:6"`
14 |
15 | User *User `gorm:"constraint:devices_user_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE"`
16 | }
17 |
18 | // TableName Device構造体のテーブル名
19 | func (*Device) TableName() string {
20 | return "devices"
21 | }
22 |
--------------------------------------------------------------------------------
/model/devices_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestDevice_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "devices", (&Device{}).TableName())
12 | }
13 |
--------------------------------------------------------------------------------
/model/json.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "database/sql/driver"
5 | "errors"
6 | )
7 |
8 | type JSON map[string]interface{}
9 |
10 | // Value database/sql/driver.Valuer 実装
11 | func (v JSON) Value() (driver.Value, error) {
12 | return json.MarshalToString(v)
13 | }
14 |
15 | // Scan database/sql.Scanner 実装
16 | func (v *JSON) Scan(src interface{}) error {
17 | switch s := src.(type) {
18 | case nil:
19 | return nil
20 | case string:
21 | return json.Unmarshal([]byte(s), v)
22 | case []byte:
23 | return json.Unmarshal(s, v)
24 | default:
25 | return errors.New("failed to scan JSON")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/model/message_report.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // MessageReport メッセージレポート構造体
11 | type MessageReport struct {
12 | ID uuid.UUID `gorm:"type:char(36);not null;primaryKey" json:"id"`
13 | MessageID uuid.UUID `gorm:"type:char(36);not null;uniqueIndex:message_reporter" json:"messageId"`
14 | Reporter uuid.UUID `gorm:"type:char(36);not null;uniqueIndex:message_reporter" json:"reporter"`
15 | Reason string `gorm:"type:TEXT COLLATE utf8mb4_bin NOT NULL" json:"reason"`
16 | CreatedAt time.Time `gorm:"precision:6;index" json:"createdAt"`
17 | DeletedAt gorm.DeletedAt `gorm:"precision:6" json:"-"`
18 | }
19 |
20 | // TableName MessageReport構造体のテーブル名
21 | func (*MessageReport) TableName() string {
22 | return "message_reports"
23 | }
24 |
--------------------------------------------------------------------------------
/model/message_report_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestMessageReport_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "message_reports", (&MessageReport{}).TableName())
12 | }
13 |
--------------------------------------------------------------------------------
/model/message_stamp.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | )
8 |
9 | // MessageStamp メッセージスタンプ構造体
10 | type MessageStamp struct {
11 | MessageID uuid.UUID `gorm:"type:char(36);not null;primaryKey;index" json:"-"`
12 | StampID uuid.UUID `gorm:"type:char(36);not null;primaryKey;index:idx_messages_stamps_user_id_stamp_id_updated_at,priority:2" json:"stampId"`
13 | UserID uuid.UUID `gorm:"type:char(36);not null;primaryKey;index:idx_messages_stamps_user_id_stamp_id_updated_at,priority:1" json:"userId"`
14 | Count int `gorm:"type:int;not null" json:"count"`
15 | CreatedAt time.Time `gorm:"precision:6" json:"createdAt"`
16 | UpdatedAt time.Time `gorm:"precision:6;index;index:idx_messages_stamps_user_id_stamp_id_updated_at,priority:3" json:"updatedAt"`
17 |
18 | Stamp *Stamp `gorm:"constraint:messages_stamps_stamp_id_stamps_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE" json:"-"`
19 | User *User `gorm:"constraint:messages_stamps_user_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE" json:"-"`
20 | }
21 |
22 | // TableName メッセージスタンプのテーブル
23 | func (*MessageStamp) TableName() string {
24 | return "messages_stamps"
25 | }
26 |
--------------------------------------------------------------------------------
/model/message_stamp_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestMessageStamp_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "messages_stamps", (&MessageStamp{}).TableName())
12 | }
13 |
--------------------------------------------------------------------------------
/model/messages_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestMessage_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "messages", (&Message{}).TableName())
12 | }
13 |
14 | func TestUnread_TableName(t *testing.T) {
15 | t.Parallel()
16 | assert.Equal(t, "unreads", (&Unread{}).TableName())
17 | }
18 |
19 | func TestChannelLatestMessage_TableName(t *testing.T) {
20 | t.Parallel()
21 | assert.Equal(t, "channel_latest_messages", (&ChannelLatestMessage{}).TableName())
22 | }
23 |
24 | func TestArchivedMessage_TableName(t *testing.T) {
25 | t.Parallel()
26 | assert.Equal(t, "archived_messages", (&ArchivedMessage{}).TableName())
27 | }
28 |
--------------------------------------------------------------------------------
/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import jsonIter "github.com/json-iterator/go"
4 |
5 | var json = jsonIter.ConfigFastest
6 |
--------------------------------------------------------------------------------
/model/pins.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | )
8 |
9 | // Pin ピン留めのレコード
10 | type Pin struct {
11 | ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
12 | MessageID uuid.UUID `gorm:"type:char(36);not null;unique"`
13 | UserID uuid.UUID `gorm:"type:char(36);not null"`
14 | CreatedAt time.Time `gorm:"precision:6"`
15 |
16 | Message Message
17 | User *User `gorm:"constraint:pins_user_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE"`
18 | }
19 |
20 | // TableName ピン留めテーブル名
21 | func (pin *Pin) TableName() string {
22 | return "pins"
23 | }
24 |
--------------------------------------------------------------------------------
/model/pins_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestPinTableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "pins", (&Pin{}).TableName())
12 | }
13 |
--------------------------------------------------------------------------------
/model/rbac.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // UserRole ユーザーロール構造体
4 | type UserRole struct {
5 | Name string `gorm:"type:varchar(30);not null;primaryKey"`
6 | Oauth2Scope bool `gorm:"type:boolean;not null;default:false"`
7 | System bool `gorm:"type:boolean;not null;default:false"`
8 |
9 | Inheritances []*UserRole `gorm:"many2many:user_role_inheritances;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:Name;references:Name;joinForeignKey:Role;joinReferences:SubRole"`
10 | Permissions []RolePermission `gorm:"constraint:user_role_permissions_role_user_roles_name_foreign,OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:Role;references:Name"`
11 | }
12 |
13 | // TableName UserRole構造体のテーブル名
14 | func (*UserRole) TableName() string {
15 | return "user_roles"
16 | }
17 |
18 | // RolePermission ロール権限構造体
19 | type RolePermission struct {
20 | Role string `gorm:"type:varchar(30);not null;primaryKey"`
21 | Permission string `gorm:"type:varchar(30);not null;primaryKey"`
22 | }
23 |
24 | // TableName RolePermission構造体のテーブル名
25 | func (*RolePermission) TableName() string {
26 | return "user_role_permissions"
27 | }
28 |
--------------------------------------------------------------------------------
/model/rbac_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestRole_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "user_roles", (&UserRole{}).TableName())
12 | }
13 |
14 | func TestRolePermission_TableName(t *testing.T) {
15 | t.Parallel()
16 | assert.Equal(t, "user_role_permissions", (&RolePermission{}).TableName())
17 | }
18 |
--------------------------------------------------------------------------------
/model/session.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "bytes"
5 | "encoding/gob"
6 | "time"
7 |
8 | "github.com/gofrs/uuid"
9 | )
10 |
11 | // SessionRecord GORM用Session構造体
12 | type SessionRecord struct {
13 | Token string `gorm:"type:varchar(50);primaryKey"`
14 | ReferenceID uuid.UUID `gorm:"type:char(36);unique"`
15 | UserID uuid.UUID `gorm:"type:varchar(36);index"`
16 | Data []byte `gorm:"type:longblob"`
17 | Created time.Time `gorm:"precision:6"`
18 | }
19 |
20 | // TableName SessionRecordのテーブル名
21 | func (*SessionRecord) TableName() string {
22 | return "r_sessions"
23 | }
24 |
25 | func (sr *SessionRecord) SetData(data map[string]interface{}) {
26 | var b bytes.Buffer
27 | if err := gob.NewEncoder(&b).Encode(data); err != nil {
28 | panic(err) // gobにdataの中身の構造体が登録されていない
29 | }
30 | sr.Data = b.Bytes()
31 | }
32 |
33 | func (sr *SessionRecord) GetData() (data map[string]interface{}, err error) {
34 | return data, gob.NewDecoder(bytes.NewReader(sr.Data)).Decode(&data)
35 | }
36 |
--------------------------------------------------------------------------------
/model/soundboard.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "github.com/gofrs/uuid"
4 |
5 | // SoundboardItem サウンドボードアイテム
6 | type SoundboardItem struct {
7 | ID uuid.UUID `gorm:"type:char(36);not null;primary_key" json:"id"`
8 | Name string `gorm:"type:varchar(32);not null" json:"name"`
9 | StampID *uuid.UUID `gorm:"type:char(36)" json:"stampId"`
10 | CreatorID uuid.UUID `gorm:"type:char(36);not null" json:"creatorId"`
11 | }
12 |
13 | // TableName サウンドボードアイテムテーブル名を取得します
14 | func (*SoundboardItem) TableName() string {
15 | return "soundboard_items"
16 | }
17 |
--------------------------------------------------------------------------------
/model/soundboard_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestSoundboardItem_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "soundboard_items", (&SoundboardItem{}).TableName())
12 | }
13 |
--------------------------------------------------------------------------------
/model/stamp_palettes.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | )
8 |
9 | type StampPalette struct {
10 | ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
11 | Name string `gorm:"type:varchar(30);not null"`
12 | Description string `gorm:"type:text;not null"`
13 | Stamps UUIDs `gorm:"type:text;not null"`
14 | CreatorID uuid.UUID `gorm:"type:char(36);not null;index"`
15 | CreatedAt time.Time `gorm:"precision:6"`
16 | UpdatedAt time.Time `gorm:"precision:6"`
17 |
18 | Creator User `gorm:"constraint:stamp_palettes_creator_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:CreatorID"`
19 | }
20 |
21 | // TableName StampPalettes構造体のテーブル名
22 | func (*StampPalette) TableName() string {
23 | return "stamp_palettes"
24 | }
25 |
--------------------------------------------------------------------------------
/model/stamps.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | "gorm.io/gorm"
8 | )
9 |
10 | // Stamp スタンプ構造体
11 | type Stamp struct {
12 | ID uuid.UUID `gorm:"type:char(36);not null;primaryKey" json:"id"`
13 | Name string `gorm:"type:varchar(32);not null;unique" json:"name"`
14 | CreatorID uuid.UUID `gorm:"type:char(36);not null" json:"creatorId"`
15 | FileID uuid.UUID `gorm:"type:char(36);not null" json:"fileId"`
16 | IsUnicode bool `gorm:"type:boolean;not null;default:false;index" json:"isUnicode"`
17 | CreatedAt time.Time `gorm:"precision:6" json:"createdAt"`
18 | UpdatedAt time.Time `gorm:"precision:6" json:"updatedAt"`
19 | DeletedAt gorm.DeletedAt `gorm:"precision:6" json:"-"`
20 |
21 | File *FileMeta `gorm:"constraint:stamps_file_id_files_id_foreign,OnUpdate:CASCADE,OnDelete:NO ACTION;foreignKey:FileID" json:"-"`
22 | }
23 |
24 | // StampWithThumbnail サムネイル情報を付与したスタンプ構造体
25 | type StampWithThumbnail struct {
26 | *Stamp
27 | HasThumbnail bool `json:"hasThumbnail"`
28 | }
29 |
30 | // TableName スタンプテーブル名を取得します
31 | func (*Stamp) TableName() string {
32 | return "stamps"
33 | }
34 |
35 | // IsSystemStamp システムが作成したスタンプかどうか
36 | func (s *Stamp) IsSystemStamp() bool {
37 | return s.CreatorID == uuid.Nil && s.ID != uuid.Nil && len(s.Name) > 0
38 | }
39 |
--------------------------------------------------------------------------------
/model/stamps_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestStamp_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "stamps", (&Stamp{}).TableName())
12 | }
13 |
--------------------------------------------------------------------------------
/model/stars.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | )
6 |
7 | // Star starの構造体
8 | type Star struct {
9 | UserID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
10 | ChannelID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
11 |
12 | User *User `gorm:"constraint:stars_user_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE"`
13 | Channel *Channel `gorm:"constraint:stars_channel_id_channels_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE"`
14 | }
15 |
16 | // TableName dbの名前を指定する
17 | func (star *Star) TableName() string {
18 | return "stars"
19 | }
20 |
--------------------------------------------------------------------------------
/model/stars_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestStar_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "stars", (&Star{}).TableName())
12 | }
13 |
--------------------------------------------------------------------------------
/model/tags_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestTag_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "tags", (&Tag{}).TableName())
12 | }
13 |
14 | func TestUsersTag_TableName(t *testing.T) {
15 | t.Parallel()
16 | assert.Equal(t, "users_tags", (&UsersTag{}).TableName())
17 | }
18 |
--------------------------------------------------------------------------------
/model/user_group_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestUserGroup_TableName(t *testing.T) {
10 | t.Parallel()
11 | assert.Equal(t, "user_groups", (&UserGroup{}).TableName())
12 | }
13 |
14 | func TestUserGroupMember_TableName(t *testing.T) {
15 | t.Parallel()
16 | assert.Equal(t, "user_group_members", (&UserGroupMember{}).TableName())
17 | }
18 |
--------------------------------------------------------------------------------
/model/user_settings.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "github.com/gofrs/uuid"
4 |
5 | // UserSettings ユーザー設定の構造体
6 | type UserSettings struct {
7 | UserID uuid.UUID `gorm:"type:char(36);not null;primaryKey;" json:"id"`
8 | NotifyCitation bool `gorm:"type:boolean" json:"notifyCitation"`
9 |
10 | User *User `gorm:"constraint:user_settings_user_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE" json:"-"`
11 | }
12 |
13 | // TableName UserSettings構造体のテーブル名
14 | func (us *UserSettings) TableName() string {
15 | return "user_settings"
16 | }
17 |
18 | // IsNotifyCitationEnabled メッセージの引用通知が有効かどうかを返します
19 | func (us *UserSettings) IsNotifyCitationEnabled() bool {
20 | return us.NotifyCitation
21 | }
22 |
--------------------------------------------------------------------------------
/model/uuids.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "database/sql/driver"
5 | "errors"
6 | "strings"
7 |
8 | "github.com/gofrs/uuid"
9 | )
10 |
11 | type UUIDs []uuid.UUID
12 |
13 | func (arr UUIDs) Value() (driver.Value, error) {
14 | idStr := make([]string, len(arr))
15 | for i, id := range arr {
16 | idStr[i] = id.String()
17 | }
18 | return strings.Join(idStr, ","), nil
19 | }
20 |
21 | func (arr *UUIDs) Scan(src interface{}) error {
22 | switch s := src.(type) {
23 | case nil:
24 | *arr = UUIDs{}
25 | case string:
26 | for _, value := range strings.Split(s, ",") {
27 | ID, err := uuid.FromString(value)
28 | if err != nil {
29 | continue
30 | }
31 | *arr = append(*arr, ID)
32 | }
33 | case []byte:
34 | for _, value := range strings.Split(string(s), ",") {
35 | ID, err := uuid.FromString(value)
36 | if err != nil {
37 | continue
38 | }
39 | *arr = append(*arr, ID)
40 | }
41 | default:
42 | return errors.New("failed to scan UUIDs")
43 | }
44 | return nil
45 | }
46 |
47 | func (arr UUIDs) ToUUIDSlice() []uuid.UUID {
48 | return arr
49 | }
50 |
--------------------------------------------------------------------------------
/repository/device.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 |
6 | "github.com/traPtitech/traQ/utils/set"
7 | )
8 |
9 | // DeviceRepository FCMデバイスリポジトリ
10 | type DeviceRepository interface {
11 | // RegisterDevice FCMデバイスを登録します
12 | //
13 | // 成功した、或いは既に登録されていた場合にnilを返します。
14 | // 引数にuuid.Nilを指定した場合、ErrNilIDを返します。
15 | // tokenが空文字列の場合、ArgumentErrorを返します。
16 | // 登録しようとしたトークンが既に他のユーザーと関連づけられていた場合はArgumentErrorを返します。
17 | // DBによるエラーを返すことがあります。
18 | RegisterDevice(userID uuid.UUID, token string) error
19 | // GetDeviceTokens 指定したユーザーの全デバイストークンを取得します
20 | //
21 | // 成功した場合、デバイストークンの配列とnilを返します。
22 | // DBによるエラーを返すことがあります。
23 | GetDeviceTokens(userIDs set.UUID) (map[uuid.UUID][]string, error)
24 | // DeleteDeviceTokens FCMデバイスの登録を解除します
25 | //
26 | // 成功した、或いは既に登録解除されていた場合にnilを返します。
27 | // DBによるエラーを返すことがあります。
28 | DeleteDeviceTokens(tokens []string) error
29 | }
30 |
--------------------------------------------------------------------------------
/repository/errors.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var (
8 | // ErrNilID 汎用エラー 引数のIDがNilです
9 | ErrNilID = errors.New("nil id")
10 | // ErrNotFound 汎用エラー 見つかりません
11 | ErrNotFound = errors.New("not found")
12 | // ErrAlreadyExists 汎用エラー 既に存在しています
13 | ErrAlreadyExists = errors.New("already exists")
14 | // ErrForbidden 汎用エラー 禁止されています
15 | ErrForbidden = errors.New("forbidden")
16 | )
17 |
18 | // ArgumentError 引数エラー
19 | type ArgumentError struct {
20 | FieldName string
21 | Message string
22 | }
23 |
24 | // Error Messageを返します
25 | func (ae *ArgumentError) Error() string {
26 | return ae.Message
27 | }
28 |
29 | // ArgError 引数エラーを発生させます
30 | func ArgError(field, message string) *ArgumentError {
31 | return &ArgumentError{FieldName: field, Message: message}
32 | }
33 |
34 | // IsArgError 引数エラーかどうか
35 | func IsArgError(err error) bool {
36 | if err == nil {
37 | return false
38 | }
39 | _, ok := err.(*ArgumentError)
40 | return ok
41 | }
42 |
--------------------------------------------------------------------------------
/repository/errors_test.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestArgumentError_Error(t *testing.T) {
10 | t.Parallel()
11 | msg := "test2"
12 | assert.Equal(t, msg, ArgError("", msg).Error())
13 | }
14 |
15 | func TestArgError(t *testing.T) {
16 | t.Parallel()
17 |
18 | f := "test1"
19 | m := "test2"
20 | err := ArgError(f, m)
21 | assert.Equal(t, f, err.FieldName)
22 | assert.Equal(t, m, err.Message)
23 | }
24 |
25 | func TestIsArgError(t *testing.T) {
26 | t.Parallel()
27 | assert.True(t, IsArgError(ArgError("", "")))
28 | assert.False(t, IsArgError(nil))
29 | assert.False(t, IsArgError(ErrAlreadyExists))
30 | }
31 |
--------------------------------------------------------------------------------
/repository/gorm/errors.go:
--------------------------------------------------------------------------------
1 | package gorm
2 |
3 | import (
4 | "gorm.io/gorm"
5 |
6 | "github.com/traPtitech/traQ/repository"
7 | )
8 |
9 | func convertError(err error) error {
10 | switch err {
11 | case gorm.ErrRecordNotFound:
12 | return repository.ErrNotFound
13 | default:
14 | return err
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/repository/gorm/repository.go:
--------------------------------------------------------------------------------
1 | package gorm
2 |
3 | import (
4 | "github.com/leandro-lugaresi/hub"
5 | "go.uber.org/zap"
6 | "gorm.io/gorm"
7 |
8 | "github.com/traPtitech/traQ/migration"
9 | "github.com/traPtitech/traQ/repository"
10 | )
11 |
12 | // Repository リポジトリ実装
13 | type Repository struct {
14 | db *gorm.DB
15 | hub *hub.Hub
16 | logger *zap.Logger
17 | repository.StampRepository
18 | repository.UserRepository
19 | }
20 |
21 | // NewGormRepository リポジトリ実装を初期化して生成します。
22 | // スキーマが初期化された場合、init: true を返します。
23 | func NewGormRepository(db *gorm.DB, hub *hub.Hub, logger *zap.Logger, doMigration bool) (repo repository.Repository, init bool, err error) {
24 | repo = &Repository{
25 | db: db,
26 | hub: hub,
27 | logger: logger.Named("repository"),
28 | StampRepository: makeStampRepository(db, hub),
29 | UserRepository: makeUserRepository(db, hub),
30 | }
31 | if doMigration {
32 | if init, err = migration.Migrate(db); err != nil {
33 | return nil, false, err
34 | }
35 | }
36 | return
37 | }
38 |
--------------------------------------------------------------------------------
/repository/gorm/soundboard.go:
--------------------------------------------------------------------------------
1 | package gorm
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | "github.com/traPtitech/traQ/model"
6 | "github.com/traPtitech/traQ/repository"
7 | )
8 |
9 | func (repo *Repository) CreateSoundboardItem(args repository.CreateSoundboardItemArgs) error {
10 | return repo.db.Create(&model.SoundboardItem{
11 | ID: args.SoundID,
12 | Name: args.SoundName,
13 | StampID: args.StampID,
14 | CreatorID: args.CreatorID,
15 | }).Error
16 | }
17 |
18 | func (repo *Repository) GetAllSoundboardItems() ([]*model.SoundboardItem, error) {
19 | items := make([]*model.SoundboardItem, 0)
20 | if err := repo.db.Find(&items).Error; err != nil {
21 | return nil, err
22 | }
23 | return items, nil
24 | }
25 |
26 | func (repo *Repository) GetSoundboardByCreatorID(creatorID uuid.UUID) ([]*model.SoundboardItem, error) {
27 | items := make([]*model.SoundboardItem, 0)
28 | if err := repo.db.Where(&model.SoundboardItem{CreatorID: creatorID}).Find(&items).Error; err != nil {
29 | return nil, err
30 | }
31 | return items, nil
32 | }
33 |
34 | func (repo *Repository) UpdateSoundboardCreatorID(soundID uuid.UUID, creatorID uuid.UUID) error {
35 | return repo.db.Model(&model.SoundboardItem{}).Where("id = ?", soundID).Update("creator_id", creatorID).Error
36 | }
37 |
38 | func (repo *Repository) DeleteSoundboardItem(soundID uuid.UUID) error {
39 | return repo.db.Delete(&model.SoundboardItem{}, soundID).Error
40 | }
41 |
--------------------------------------------------------------------------------
/repository/gorm/user_role.go:
--------------------------------------------------------------------------------
1 | package gorm
2 |
3 | import "github.com/traPtitech/traQ/model"
4 |
5 | // CreateUserRoles implements UserRoleRepository interface.
6 | func (repo *Repository) CreateUserRoles(roles ...*model.UserRole) error {
7 | return repo.db.Create(roles).Error
8 | }
9 |
10 | // GetAllUserRoles implements UserRoleRepository interface.
11 | func (repo *Repository) GetAllUserRoles() ([]*model.UserRole, error) {
12 | var roles []*model.UserRole
13 | err := repo.db.Preload("Inheritances").Preload("Permissions").Find(&roles).Error
14 | return roles, err
15 | }
16 |
--------------------------------------------------------------------------------
/repository/message_report.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // MessageReportRepository メッセージ通報リポジトリ
10 | type MessageReportRepository interface {
11 | // CreateMessageReport 指定したユーザーによる指定したメッセージの通報を登録します
12 | //
13 | // 成功した場合、nilを返します。
14 | // 既に通報がされていた場合、ErrAlreadyExistsを返します。
15 | // 引数にuuid.Nilを指定するとErrNilIDを返します。
16 | // DBによるエラーを返すことがあります。
17 | CreateMessageReport(messageID, reporterID uuid.UUID, reason string) error
18 | // GetMessageReports メッセージ通報を通報日時の昇順で取得します
19 | //
20 | // 成功した場合、メッセージ通報の配列とnilを返します。負のoffset, limitは無視されます。
21 | // DBによるエラーを返すことがあります。
22 | GetMessageReports(offset, limit int) ([]*model.MessageReport, error)
23 | // GetMessageReportsByMessageID 指定したメッセージのメッセージ通報を全て取得します
24 | //
25 | // 成功した場合、メッセージ通報の配列とnilを返します。
26 | // 存在しないメッセージを指定した場合は空配列とnilを返します。
27 | // DBによるエラーを返すことがあります。
28 | GetMessageReportsByMessageID(messageID uuid.UUID) ([]*model.MessageReport, error)
29 | // GetMessageReportsByReporterID 指定したユーザーによるメッセージ通報を全て取得します
30 | //
31 | // 成功した場合、メッセージ通報の配列とnilを返します。
32 | // 存在しないユーザーを指定した場合は空配列とnilを返します。
33 | // DBによるエラーを返すことがあります。
34 | GetMessageReportsByReporterID(reporterID uuid.UUID) ([]*model.MessageReport, error)
35 | }
36 |
--------------------------------------------------------------------------------
/repository/ogp_cache.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | type OgpCacheRepository interface {
10 | // CreateOgpCache OGPキャッシュを作成します
11 | //
12 | // contentがnilの場合、Validをfalseとしたネガティブキャッシュを作成します。
13 | //
14 | // 成功した場合、作成されたOGPキャッシュとnilを返します。
15 | // DBによるエラーを返すことがあります。
16 | CreateOgpCache(url string, content *model.Ogp, cacheFor time.Duration) (c *model.OgpCache, err error)
17 |
18 | // GetOgpCache 指定したURLのOGPキャッシュを取得します
19 | //
20 | // 成功した場合、取得したOGPキャッシュとnilを返します。
21 | // 存在しなかった場合、ErrNotFoundを返します。
22 | // DBによるエラーを返すことがあります。
23 | GetOgpCache(url string) (c *model.OgpCache, err error)
24 |
25 | // DeleteOgpCache 指定したURLのOGPキャッシュを削除します
26 | //
27 | // 成功した場合、nilを返します。
28 | // 存在しなかった場合、ErrNotFoundを返します。
29 | // DBによるエラーを返すことがあります。
30 | DeleteOgpCache(url string) error
31 |
32 | // DeleteStaleOgpCache 保存期間が経過したOGPキャッシュを削除します
33 | //
34 | // 成功した場合、nilを返します。
35 | // DBによるエラーを返すことがあります。
36 | DeleteStaleOgpCache() error
37 | }
38 |
--------------------------------------------------------------------------------
/repository/pin.go:
--------------------------------------------------------------------------------
1 | //go:generate mockgen -source=$GOFILE -destination=mock_$GOPACKAGE/mock_$GOFILE
2 | package repository
3 |
4 | import (
5 | "github.com/gofrs/uuid"
6 |
7 | "github.com/traPtitech/traQ/model"
8 | )
9 |
10 | // PinRepository ピンリポジトリ
11 | type PinRepository interface {
12 | // PinMessage 指定したユーザーによって指定したメッセージをピン留めします
13 | PinMessage(messageID, userID uuid.UUID) (*model.Pin, error)
14 | // UnpinMessage 指定したユーザーによって指定したピン留めを削除します
15 | UnpinMessage(messageID uuid.UUID) (*model.Pin, error)
16 | // GetPinnedMessageByChannelID 指定したチャンネルのピン留めを全て取得します
17 | GetPinnedMessageByChannelID(channelID uuid.UUID) ([]*model.Pin, error)
18 | }
19 |
--------------------------------------------------------------------------------
/repository/repository.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | // Repository データリポジトリ
4 | type Repository interface {
5 | UserRepository
6 | UserGroupRepository
7 | UserSettingsRepository
8 | UserRoleRepository
9 | TagRepository
10 | ChannelRepository
11 | MessageRepository
12 | MessageReportRepository
13 | StampRepository
14 | StampPaletteRepository
15 | StarRepository
16 | PinRepository
17 | DeviceRepository
18 | FileRepository
19 | WebhookRepository
20 | OAuth2Repository
21 | BotRepository
22 | ClipRepository
23 | OgpCacheRepository
24 | SoundboardRepository
25 | }
26 |
--------------------------------------------------------------------------------
/repository/soundboard.go:
--------------------------------------------------------------------------------
1 | //go:generate mockgen -source=$GOFILE -destination=mock_$GOPACKAGE/mock_$GOFILE
2 | package repository
3 |
4 | import (
5 | "github.com/gofrs/uuid"
6 |
7 | "github.com/traPtitech/traQ/model"
8 | )
9 |
10 | // CreateSoundboardItemArgs サウンドボードアイテム作成引数
11 | type CreateSoundboardItemArgs struct {
12 | SoundID uuid.UUID
13 | SoundName string
14 | StampID *uuid.UUID
15 | CreatorID uuid.UUID
16 | }
17 |
18 | // SoundboardRepository サウンドボードリポジトリ
19 | type SoundboardRepository interface {
20 | // CreateSoundboardItem サウンドボードアイテムを作成します
21 | //
22 | // 成功した場合、nilを返します。
23 | // 引数に問題がある場合、ArgumentErrorを返します。
24 | // DBによるエラーを返すことがあります。
25 | CreateSoundboardItem(args CreateSoundboardItemArgs) error
26 | // GetAllSoundboardItems すべてのサウンドボードアイテムを取得します
27 | //
28 | // 成功した場合、サウンドボードアイテムの配列とnilを返します。
29 | // DBによるエラーを返すことがあります。
30 | GetAllSoundboardItems() ([]*model.SoundboardItem, error)
31 | // GetSoundboardItem 指定したIDのユーザーが作成したサウンドボードアイテムを取得します
32 | //
33 | // 成功した場合、サウンドボードアイテムとnilを返します。
34 | // 存在しなかった場合、ErrNotFoundを返します。
35 | // DBによるエラーを返すことがあります。
36 | GetSoundboardByCreatorID(creatorID uuid.UUID) ([]*model.SoundboardItem, error)
37 | // UpdateSoundboardCreatorID サウンドボードアイテムの作成者を更新します
38 | //
39 | // 成功した場合、nilを返します。
40 | // 存在しなかった場合、ErrNotFoundを返します。
41 | // DBによるエラーを返すことがあります。
42 | UpdateSoundboardCreatorID(soundID uuid.UUID, creatorID uuid.UUID) error
43 | // DeleteSoundboardItem サウンドボードアイテムを削除します
44 | //
45 | // 成功した場合、nilを返します。
46 | // 存在しなかった場合、ErrNotFoundを返します。
47 | // DBによるエラーを返すことがあります。
48 | DeleteSoundboardItem(soundID uuid.UUID) error
49 | }
50 |
--------------------------------------------------------------------------------
/repository/star.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import "github.com/gofrs/uuid"
4 |
5 | // StarRepository チャンネルスターリポジトリ
6 | type StarRepository interface {
7 | // AddStar チャンネルをお気に入り登録します
8 | //
9 | // 成功した、或いは既に登録されていた場合にnilを返します。
10 | // 引数にuuid.Nilを指定するとErrNilIDを返します。
11 | // DBによるエラーを返すことがあります。
12 | AddStar(userID, channelID uuid.UUID) error
13 | // RemoveStar チャンネルのお気に入りを解除します
14 | //
15 | // 成功した、或いは既に解除されていた場合にnilを返します。
16 | // 引数にuuid.Nilを指定するとErrNilIDを返します。
17 | // DBによるエラーを返すことがあります。
18 | RemoveStar(userID, channelID uuid.UUID) error
19 | // GetStaredChannels ユーザーがお気に入りをしているチャンネルIDを取得します
20 | //
21 | // 成功した場合、チャンネルUUIDの配列とnilを返します。
22 | // 存在しないユーザーを指定した場合は空配列とnilを返します。
23 | // DBによるエラーを返すことがあります。
24 | GetStaredChannels(userID uuid.UUID) ([]uuid.UUID, error)
25 | }
26 |
--------------------------------------------------------------------------------
/repository/user_role.go:
--------------------------------------------------------------------------------
1 | //go:generate mockgen -source=$GOFILE -destination=mock_$GOPACKAGE/mock_$GOFILE
2 | package repository
3 |
4 | import "github.com/traPtitech/traQ/model"
5 |
6 | type UserRoleRepository interface {
7 | // CreateUserRoles ユーザーロールを作成します
8 | //
9 | // 成功した場合、nilを返します。
10 | // DBによるエラーを返すことがあります。
11 | CreateUserRoles(roles ...*model.UserRole) error
12 | // GetAllUserRoles 全てのユーザーロールを返します
13 | //
14 | // 成功した場合、ユーザーロールの配列とnilを返します。
15 | // DBによるエラーを返すことがあります。
16 | GetAllUserRoles() ([]*model.UserRole, error)
17 | }
18 |
--------------------------------------------------------------------------------
/repository/user_settings.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // UserSettingsRepository ユーザセッティングレポジトリ
10 | type UserSettingsRepository interface {
11 | // UpdateNotifyCitation メッセージ引用通知を設定します
12 | //
13 | // isEnableがtrueの場合、メッセージ引用通知を有効にします
14 | // isEnableがfalseの場合、メッセージ引用通知を無効にします
15 | // DBによるエラーを返すことがあります
16 | UpdateNotifyCitation(userID uuid.UUID, isEnable bool) error
17 | // GetNotifyCitation メッセージ引用通知の情報を取得します
18 | //
19 | // 返り値がtrueの場合、メッセージ引用通知が有効です
20 | // 返り値がfalseの場合、メッセージ引用通知が無効です
21 | // DBによるエラーを返すことがあります
22 | GetNotifyCitation(userID uuid.UUID) (bool, error)
23 | // GetUserSettings ユーザー設定を返します
24 | // DBによるエラーを返すことがあります
25 | GetUserSettings(userID uuid.UUID) (*model.UserSettings, error)
26 | }
27 |
--------------------------------------------------------------------------------
/repository/user_test.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/gofrs/uuid"
7 | "github.com/stretchr/testify/assert"
8 |
9 | "github.com/traPtitech/traQ/utils/optional"
10 | )
11 |
12 | func TestUsersQuery_Active(t *testing.T) {
13 | t.Parallel()
14 |
15 | assert.EqualValues(t,
16 | UsersQuery{IsActive: optional.From(true)},
17 | UsersQuery{}.Active(),
18 | )
19 | }
20 |
21 | func TestUsersQuery_NotBot(t *testing.T) {
22 | t.Parallel()
23 |
24 | assert.EqualValues(t,
25 | UsersQuery{IsBot: optional.From(false)},
26 | UsersQuery{}.NotBot(),
27 | )
28 | }
29 |
30 | func TestUsersQuery_CMemberOf(t *testing.T) {
31 | t.Parallel()
32 |
33 | id, _ := uuid.NewV7()
34 | assert.EqualValues(t,
35 | UsersQuery{IsCMemberOf: optional.From(id)},
36 | UsersQuery{}.CMemberOf(id),
37 | )
38 | }
39 |
40 | func TestUsersQuery_GMemberOf(t *testing.T) {
41 | t.Parallel()
42 |
43 | id, _ := uuid.NewV7()
44 | assert.EqualValues(t,
45 | UsersQuery{IsGMemberOf: optional.From(id)},
46 | UsersQuery{}.GMemberOf(id),
47 | )
48 | }
49 |
50 | func TestUsersQuery_Composite(t *testing.T) {
51 | t.Parallel()
52 |
53 | assert.EqualValues(t,
54 | UsersQuery{IsActive: optional.From(true), IsBot: optional.From(false)},
55 | UsersQuery{}.NotBot().Active(),
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/router/consts/headers.go:
--------------------------------------------------------------------------------
1 | package consts
2 |
3 | const (
4 | HeaderCacheControl = "Cache-Control"
5 | HeaderETag = "ETag"
6 | HeaderIfMatch = "If-Match"
7 | HeaderIfNoneMatch = "If-None-Match"
8 | HeaderIfModifiedSince = "If-Modified-Since"
9 | HeaderIfUnmodifiedSince = "If-Unmodified-Since"
10 | HeaderFileMetaType = "X-TRAQ-FILE-TYPE"
11 | HeaderCacheFile = "X-TRAQ-FILE-CACHE"
12 | HeaderSignature = "X-TRAQ-Signature"
13 | HeaderChannelID = "X-TRAQ-Channel-Id"
14 | HeaderMore = "X-TRAQ-More"
15 | HeaderVersion = "X-TRAQ-VERSION"
16 | )
17 |
--------------------------------------------------------------------------------
/router/consts/keys.go:
--------------------------------------------------------------------------------
1 | package consts
2 |
3 | const (
4 | KeyUserID = "userID"
5 | KeyUser = "user"
6 | KeyOAuth2AccessScopes = "scopes"
7 | KeyParamStamp = "paramStamp"
8 | KeyParamStampPalette = "paramStampPalette"
9 | KeyParamGroup = "paramGroup"
10 | KeyParamUser = "paramUser"
11 | KeyParamClient = "paramClient"
12 | KeyParamBot = "paramBot"
13 | KeyParamWebhook = "paramWebhook"
14 | KeyParamMessage = "paramMessage"
15 | KeyParamChannel = "paramChannel"
16 | KeyParamFile = "paramFile"
17 | KeyParamClipFolder = "paramClipFolder"
18 | KeyRepo = "_repo"
19 | KeyChannelManager = "_cm"
20 | )
21 |
--------------------------------------------------------------------------------
/router/consts/mime_types.go:
--------------------------------------------------------------------------------
1 | package consts
2 |
3 | const (
4 | MimeImagePNG = "image/png"
5 | MimeImageJPEG = "image/jpeg"
6 | MimeImageGIF = "image/gif"
7 | MimeImageSVG = "image/svg+xml"
8 | )
9 |
--------------------------------------------------------------------------------
/router/consts/parameters.go:
--------------------------------------------------------------------------------
1 | package consts
2 |
3 | const (
4 | ParamChannelID = "channelID"
5 | ParamPinID = "pinID"
6 | ParamUserID = "userID"
7 | ParamUsername = "username"
8 | ParamGroupID = "groupID"
9 | ParamTagID = "tagID"
10 | ParamStampID = "stampID"
11 | ParamStampPaletteID = "paletteID"
12 | ParamMessageID = "messageID"
13 | ParamReferenceID = "referenceID"
14 | ParamFileID = "fileID"
15 | ParamWebhookID = "webhookID"
16 | ParamTokenID = "tokenID"
17 | ParamBotID = "botID"
18 | ParamClientID = "clientID"
19 | ParamClipFolderID = "folderID"
20 | ParamURL = "url"
21 | )
22 |
--------------------------------------------------------------------------------
/router/consts/stamp_type.go:
--------------------------------------------------------------------------------
1 | package consts
2 |
3 | const (
4 | StampTypeUnicode = "unicode"
5 | StampTypeOriginal = "original"
6 | )
7 |
--------------------------------------------------------------------------------
/router/extension/ctxkey/extension.go:
--------------------------------------------------------------------------------
1 | package ctxkey
2 |
3 | // ctxKey context.Context用のキータイプ
4 | type ctxKey int
5 |
6 | const (
7 | // UserID ユーザーUUIDキー
8 | UserID ctxKey = iota
9 | )
10 |
--------------------------------------------------------------------------------
/router/extension/herror/500.go:
--------------------------------------------------------------------------------
1 | package herror
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "runtime/debug"
7 |
8 | "github.com/blendle/zapdriver"
9 | "go.uber.org/zap"
10 | )
11 |
12 | // InternalError 内部エラー
13 | type InternalError struct {
14 | // Err エラー
15 | Err error
16 | // Stack スタックトレース
17 | Stack []byte
18 | // Fields zapログ用フィールド
19 | Fields []zap.Field
20 | // Panic panicが発生したかどうか
21 | Panic bool
22 | }
23 |
24 | func (i *InternalError) Error() string {
25 | if i.Panic {
26 | return fmt.Sprintf("[Panic] %s\n%s", i.Err.Error(), i.Stack)
27 | }
28 | return fmt.Sprintf("%s\n%s", i.Err.Error(), i.Stack)
29 | }
30 |
31 | func InternalServerError(err error) error {
32 | return &InternalError{
33 | Err: err,
34 | Stack: debug.Stack(),
35 | Fields: []zap.Field{zapdriver.ErrorReport(runtime.Caller(1)), zap.Error(err)},
36 | Panic: false,
37 | }
38 | }
39 |
40 | func Panic(err error) error {
41 | return &InternalError{
42 | Err: err,
43 | Stack: debug.Stack(),
44 | Fields: []zap.Field{zapdriver.ErrorReport(runtime.Caller(1)), zap.Error(err)},
45 | Panic: true,
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/router/extension/herror/errors.go:
--------------------------------------------------------------------------------
1 | package herror
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 |
8 | "github.com/traPtitech/traQ/repository"
9 | )
10 |
11 | func NotFound(err ...interface{}) error {
12 | return HTTPError(http.StatusNotFound, err)
13 | }
14 |
15 | func BadRequest(err ...interface{}) error {
16 | return HTTPError(http.StatusBadRequest, err)
17 | }
18 |
19 | func Forbidden(err ...interface{}) error {
20 | return HTTPError(http.StatusForbidden, err)
21 | }
22 |
23 | func Conflict(err ...interface{}) error {
24 | return HTTPError(http.StatusConflict, err)
25 | }
26 |
27 | func Unauthorized(err ...interface{}) error {
28 | return HTTPError(http.StatusUnauthorized, err)
29 | }
30 |
31 | func HTTPError(code int, err interface{}) error {
32 | switch v := err.(type) {
33 | case []interface{}:
34 | if len(v) > 0 {
35 | return HTTPError(code, v[0])
36 | }
37 | return HTTPError(code, nil)
38 | case string:
39 | return echo.NewHTTPError(code, v)
40 | case *repository.ArgumentError:
41 | return echo.NewHTTPError(code, v.Error())
42 | case nil:
43 | return echo.NewHTTPError(code)
44 | default:
45 | return echo.NewHTTPError(code, v)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/router/extension/logger.go:
--------------------------------------------------------------------------------
1 | package extension
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 |
6 | "github.com/traPtitech/traQ/utils/random"
7 | )
8 |
9 | // GetRequestID リクエストIDを返します
10 | func GetRequestID(c echo.Context) string {
11 | rid := c.Request().Header.Get(echo.HeaderXRequestID)
12 | if len(rid) == 0 {
13 | rid = random.AlphaNumeric(32)
14 | }
15 | return rid
16 | }
17 |
--------------------------------------------------------------------------------
/router/middlewares/body_limit.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/labstack/echo/v4"
8 | )
9 |
10 | // RequestBodyLengthLimit リクエストボディのContentLengthで制限をかけるミドルウェア
11 | func RequestBodyLengthLimit(kib int64) echo.MiddlewareFunc {
12 | limit := kib << 10
13 | return func(next echo.HandlerFunc) echo.HandlerFunc {
14 | return func(c echo.Context) error {
15 | if l := c.Request().Header.Get(echo.HeaderContentLength); len(l) == 0 {
16 | return echo.NewHTTPError(http.StatusLengthRequired) // ContentLengthを送ってこないリクエストを殺す
17 | }
18 | if c.Request().ContentLength > limit {
19 | return echo.NewHTTPError(http.StatusRequestEntityTooLarge, fmt.Sprintf("the request must be smaller than %dKiB", kib))
20 | }
21 | return next(c)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/router/middlewares/gzip.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "compress/gzip"
5 | "net/http"
6 |
7 | "github.com/NYTimes/gziphandler"
8 | "github.com/labstack/echo/v4"
9 | )
10 |
11 | // Gzip Gzipミドルウェア
12 | func Gzip() echo.MiddlewareFunc {
13 | gzh, _ := gziphandler.GzipHandlerWithOpts(
14 | gziphandler.ContentTypes([]string{
15 | "application/javascript",
16 | "application/json",
17 | "image/svg+xml",
18 | "text/css",
19 | "text/html",
20 | "text/plain",
21 | "text/xml",
22 | }),
23 | gziphandler.CompressionLevel(gzip.BestSpeed),
24 | )
25 | return func(next echo.HandlerFunc) echo.HandlerFunc {
26 | return func(c echo.Context) (err error) {
27 | gzh(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28 | c.SetRequest(r)
29 | c.Response().Writer = w
30 | if err := next(c); err != nil {
31 | c.Error(err)
32 | }
33 | })).ServeHTTP(c.Response().Writer, c.Request())
34 | return
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/router/middlewares/no_login.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 |
6 | "github.com/traPtitech/traQ/repository"
7 | "github.com/traPtitech/traQ/router/extension/herror"
8 | "github.com/traPtitech/traQ/router/session"
9 | )
10 |
11 | // NoLogin セッションが既に存在するリクエストを拒否するミドルウェア
12 | func NoLogin(sessStore session.Store, repo repository.Repository) echo.MiddlewareFunc {
13 | return func(next echo.HandlerFunc) echo.HandlerFunc {
14 | return func(c echo.Context) error {
15 | if len(c.Request().Header.Get(echo.HeaderAuthorization)) > 0 {
16 | return herror.BadRequest("Authorization Header must not be set. Please logout once.")
17 | }
18 |
19 | sess, err := sessStore.GetSession(c)
20 | if err != nil {
21 | return herror.InternalServerError(err)
22 | }
23 | if sess != nil && sess.LoggedIn() {
24 | user, err := repo.GetUser(sess.UserID(), false)
25 | if err != nil {
26 | return herror.InternalServerError(err)
27 | }
28 | if !user.IsActive() {
29 | return herror.Forbidden("this account is currently suspended")
30 | }
31 | return herror.BadRequest("You have already logged in. Please logout once.")
32 | }
33 |
34 | return next(c)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/router/middlewares/precond.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/labstack/echo/v4"
7 |
8 | "github.com/traPtitech/traQ/router/extension"
9 | )
10 |
11 | // CheckModTimePrecondition 事前条件検査ミドルウェア
12 | func CheckModTimePrecondition(modTimeFunc func(c echo.Context) time.Time, preFunc ...echo.HandlerFunc) echo.MiddlewareFunc {
13 | return func(next echo.HandlerFunc) echo.HandlerFunc {
14 | return func(c echo.Context) error {
15 | if len(preFunc) > 0 {
16 | if err := preFunc[0](c); err != nil {
17 | return err
18 | }
19 | }
20 | modTime := modTimeFunc(c)
21 | extension.SetLastModified(c, modTime)
22 | if ok, _ := extension.CheckPreconditions(c, modTime); ok {
23 | return nil
24 | }
25 | return next(c)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/router/middlewares/recovery.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os"
7 | "strings"
8 |
9 | "github.com/labstack/echo/v4"
10 | "go.uber.org/zap"
11 |
12 | "github.com/traPtitech/traQ/router/extension"
13 | "github.com/traPtitech/traQ/router/extension/herror"
14 | )
15 |
16 | // Recovery Recoveryミドルウェア
17 | func Recovery(logger *zap.Logger) echo.MiddlewareFunc {
18 | return func(next echo.HandlerFunc) echo.HandlerFunc {
19 | return func(c echo.Context) (err error) {
20 | defer func() {
21 | if r := recover(); r != nil {
22 | pe, ok := r.(error)
23 | if !ok {
24 | pe = fmt.Errorf("%v", r)
25 | }
26 |
27 | if ne, ok := pe.(*net.OpError); ok {
28 | if se, ok := ne.Err.(*os.SyscallError); ok {
29 | if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
30 | logger.Warn(pe.Error(),
31 | zap.String("requestId", extension.GetRequestID(c)),
32 | zap.Error(pe),
33 | )
34 | err = nil
35 | return
36 | }
37 | }
38 | }
39 |
40 | err = herror.Panic(pe)
41 | }
42 | }()
43 | return next(c)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/router/middlewares/request_counter.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/prometheus/client_golang/prometheus"
8 | "github.com/prometheus/client_golang/prometheus/promauto"
9 | )
10 |
11 | var requestCounter = promauto.NewCounterVec(prometheus.CounterOpts{
12 | Namespace: "traq",
13 | Name: "http_requests_total",
14 | }, []string{"code", "method"})
15 |
16 | // RequestCounter prometheus metrics用リクエストカウンター
17 | func RequestCounter() echo.MiddlewareFunc {
18 | return func(next echo.HandlerFunc) echo.HandlerFunc {
19 | return func(c echo.Context) (err error) {
20 | err = next(c)
21 | requestCounter.WithLabelValues(strconv.Itoa(c.Response().Status), c.Request().Method).Inc()
22 | return err
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/router/middlewares/request_id.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 |
6 | "github.com/traPtitech/traQ/router/extension"
7 | )
8 |
9 | // RequestID リクエストIDを生成するミドルウェア
10 | func RequestID() echo.MiddlewareFunc {
11 | return func(next echo.HandlerFunc) echo.HandlerFunc {
12 | return func(c echo.Context) error {
13 | c.Response().Header().Set(echo.HeaderXRequestID, extension.GetRequestID(c))
14 | return next(c)
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/router/middlewares/server_version.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 |
6 | "github.com/traPtitech/traQ/router/consts"
7 | )
8 |
9 | // ServerVersion X-TRAQ-VERSIONレスポンスヘッダーを追加するミドルウェア
10 | func ServerVersion(version string) echo.MiddlewareFunc {
11 | return func(next echo.HandlerFunc) echo.HandlerFunc {
12 | return func(c echo.Context) error {
13 | c.Response().Header().Set(consts.HeaderVersion, version)
14 | return next(c)
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/router/oauth2/oidc.go:
--------------------------------------------------------------------------------
1 | package oauth2
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/zitadel/oidc/v3/pkg/oidc"
8 |
9 | "github.com/traPtitech/traQ/utils"
10 | "github.com/traPtitech/traQ/utils/jwt"
11 | )
12 |
13 | // OIDCDiscovery OpenID Connect Discovery のハンドラ
14 | func (h *Handler) OIDCDiscovery(c echo.Context) error {
15 | return c.JSON(http.StatusOK, &oidc.DiscoveryConfiguration{
16 | Issuer: h.Origin,
17 | AuthorizationEndpoint: h.Origin + "/api/v3/oauth2/authorize",
18 | TokenEndpoint: h.Origin + "/api/v3/oauth2/token",
19 | UserinfoEndpoint: h.Origin + "/api/v3/users/me/oidc",
20 | RevocationEndpoint: h.Origin + "/api/v3/oauth2/revoke",
21 | EndSessionEndpoint: h.Origin + "/api/v3/logout",
22 | JwksURI: h.Origin + "/api/v3/jwks",
23 | ScopesSupported: supportedScopes,
24 | ResponseTypesSupported: supportedResponseTypes,
25 | ResponseModesSupported: supportedResponseModes,
26 | GrantTypesSupported: utils.Map(supportedGrantTypes, func(s string) oidc.GrantType { return oidc.GrantType(s) }),
27 | SubjectTypesSupported: []string{"public"},
28 | IDTokenSigningAlgValuesSupported: jwt.SupportedAlgorithms(),
29 | CodeChallengeMethodsSupported: utils.Map(supportedCodeChallengeMethods, func(s string) oidc.CodeChallengeMethod { return oidc.CodeChallengeMethod(s) }),
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/router/oauth2/revoke_token_endpoint.go:
--------------------------------------------------------------------------------
1 | package oauth2
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 |
8 | "github.com/traPtitech/traQ/router/extension/herror"
9 | )
10 |
11 | // RevokeTokenEndpointHandler トークン無効化エンドポイントのハンドラ
12 | func (h *Handler) RevokeTokenEndpointHandler(c echo.Context) error {
13 | var req struct {
14 | Token string `form:"token"`
15 | }
16 | if err := c.Bind(&req); err != nil {
17 | return herror.BadRequest(err)
18 | }
19 |
20 | if len(req.Token) == 0 {
21 | return c.NoContent(http.StatusOK)
22 | }
23 |
24 | if err := h.Repo.DeleteTokenByAccess(req.Token); err != nil {
25 | return herror.InternalServerError(err)
26 | }
27 | if err := h.Repo.DeleteTokenByRefresh(req.Token); err != nil {
28 | return herror.InternalServerError(err)
29 | }
30 |
31 | return c.NoContent(http.StatusOK)
32 | }
33 |
--------------------------------------------------------------------------------
/router/router_wire.go:
--------------------------------------------------------------------------------
1 | //go:build wireinject
2 | // +build wireinject
3 |
4 | package router
5 |
6 | import (
7 | "github.com/google/wire"
8 | "github.com/leandro-lugaresi/hub"
9 | "go.uber.org/zap"
10 | "gorm.io/gorm"
11 |
12 | "github.com/traPtitech/traQ/repository"
13 | "github.com/traPtitech/traQ/router/oauth2"
14 | "github.com/traPtitech/traQ/router/session"
15 | "github.com/traPtitech/traQ/router/utils"
16 | v1 "github.com/traPtitech/traQ/router/v1"
17 | v3 "github.com/traPtitech/traQ/router/v3"
18 | "github.com/traPtitech/traQ/service"
19 | "github.com/traPtitech/traQ/utils/message"
20 | )
21 |
22 | func newRouter(hub *hub.Hub, db *gorm.DB, repo repository.Repository, ss *service.Services, logger *zap.Logger, config *Config) *Router {
23 | wire.Build(
24 | service.ProviderSet,
25 | newEcho,
26 | utils.NewReplaceMapper,
27 | message.NewReplacer,
28 | v1.NewEmojiCache,
29 | provideOAuth2Config,
30 | provideV3Config,
31 | session.NewGormStore,
32 | wire.Struct(new(v1.Handlers), "*"),
33 | wire.Struct(new(v3.Handlers), "*"),
34 | wire.Struct(new(oauth2.Handler), "*"),
35 | wire.Struct(new(Router), "*"),
36 | )
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/router/session/session.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/labstack/echo/v4"
9 | )
10 |
11 | const (
12 | // CookieName セッションクッキー名
13 | CookieName = "r_session"
14 | sessionMaxAge = 60 * 60 * 24 * 14 // 2 weeks
15 | sessionKeepAge = 60 * 60 * 24 * 14 // 2 weeks
16 | cacheSize = 2048
17 | )
18 |
19 | var ErrSessionNotFound = errors.New("session not found")
20 |
21 | type Session interface {
22 | Token() string
23 | RefID() uuid.UUID
24 | UserID() uuid.UUID
25 | CreatedAt() time.Time
26 | LoggedIn() bool
27 |
28 | Get(key string) (interface{}, error)
29 | Set(key string, value interface{}) error
30 | Delete(key string) error
31 |
32 | Expired() bool
33 | Refreshable() bool
34 | }
35 |
36 | type Store interface {
37 | GetSession(c echo.Context) (Session, error)
38 | GetSessionByToken(token string) (Session, error)
39 | GetSessionsByUserID(userID uuid.UUID) ([]Session, error)
40 | RevokeSession(c echo.Context) error
41 | RevokeSessionByRefID(refID uuid.UUID) error
42 | RevokeSessionsByUserID(userID uuid.UUID) error
43 | RenewSession(c echo.Context, userID uuid.UUID) (Session, error)
44 | IssueSession(userID uuid.UUID, data map[string]interface{}) (Session, error)
45 | }
46 |
--------------------------------------------------------------------------------
/router/utils/replace_mapper.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 |
6 | "github.com/traPtitech/traQ/repository"
7 | "github.com/traPtitech/traQ/service/channel"
8 | "github.com/traPtitech/traQ/utils/message"
9 | )
10 |
11 | type replaceMapperImpl struct {
12 | repo repository.Repository
13 | cm channel.Manager
14 | }
15 |
16 | func (m *replaceMapperImpl) Channel(path string) (uuid.UUID, bool) {
17 | id := m.cm.PublicChannelTree().GetChannelIDFromPath(path)
18 | return id, id != uuid.Nil
19 | }
20 |
21 | func (m *replaceMapperImpl) Group(name string) (uuid.UUID, bool) {
22 | g, err := m.repo.GetUserGroupByName(name)
23 | if err != nil {
24 | return uuid.Nil, false
25 | }
26 | return g.ID, true
27 | }
28 |
29 | func (m *replaceMapperImpl) User(name string) (uuid.UUID, bool) {
30 | u, err := m.repo.GetUserByName(name, false)
31 | if err != nil {
32 | return uuid.Nil, false
33 | }
34 | return u.GetID(), true
35 | }
36 |
37 | func NewReplaceMapper(repo repository.Repository, cm channel.Manager) message.ReplaceMapper {
38 | return &replaceMapperImpl{
39 | repo: repo,
40 | cm: cm,
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/router/v1/files.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 |
8 | "github.com/traPtitech/traQ/router/consts"
9 | "github.com/traPtitech/traQ/router/utils"
10 | )
11 |
12 | // GetFileByID GET /files/:fileID
13 | func (h *Handlers) GetFileByID(c echo.Context) error {
14 | return utils.ServeFile(c, getFileFromContext(c))
15 | }
16 |
17 | // GetMetaDataByFileID GET /files/:fileID/meta
18 | func (h *Handlers) GetMetaDataByFileID(c echo.Context) error {
19 | meta := getFileFromContext(c)
20 | c.Response().Header().Set(consts.HeaderCacheControl, "private, max-age=86400") // 1日キャッシュ
21 | return c.JSON(http.StatusOK, formatFile(meta))
22 | }
23 |
24 | // GetThumbnailByID GET /files/:fileID/thumbnail
25 | func (h *Handlers) GetThumbnailByID(c echo.Context) error {
26 | return utils.ServeFileThumbnail(c, getFileFromContext(c), h.Repo, h.Logger)
27 | }
28 |
--------------------------------------------------------------------------------
/router/v1/responses.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | )
10 |
11 | type fileResponse struct {
12 | FileID uuid.UUID `json:"fileId"`
13 | Name string `json:"name"`
14 | Mime string `json:"mime"`
15 | Size int64 `json:"size"`
16 | MD5 string `json:"md5"`
17 | HasThumb bool `json:"hasThumb"`
18 | ThumbWidth int `json:"thumbWidth,omitempty"`
19 | ThumbHeight int `json:"thumbHeight,omitempty"`
20 | Datetime time.Time `json:"datetime"`
21 | }
22 |
23 | func formatFile(f model.File) *fileResponse {
24 | hasThumb, t := f.GetThumbnail(model.ThumbnailTypeImage)
25 | return &fileResponse{
26 | FileID: f.GetID(),
27 | Name: f.GetFileName(),
28 | Mime: f.GetMIMEType(),
29 | Size: f.GetFileSize(),
30 | MD5: f.GetMD5Hash(),
31 | HasThumb: hasThumb,
32 | ThumbWidth: t.Width,
33 | ThumbHeight: t.Height,
34 | Datetime: f.GetCreatedAt(),
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/router/v3/ogp.go:
--------------------------------------------------------------------------------
1 | package v3
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 |
7 | "github.com/labstack/echo/v4"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | "github.com/traPtitech/traQ/router/consts"
11 | "github.com/traPtitech/traQ/router/extension/herror"
12 | )
13 |
14 | type CacheHitState int
15 |
16 | // GetOgp GET /ogp?url={url}
17 | func (h *Handlers) GetOgp(c echo.Context) error {
18 | u, parseErr := url.Parse(c.QueryParam(consts.ParamURL))
19 | if parseErr != nil || len(u.Scheme) == 0 || len(u.Host) == 0 {
20 | return herror.BadRequest("invalid url")
21 | }
22 |
23 | // キャッシュの削除に対応するために Cache-Control でのキャッシュはしない
24 | res, _, err := h.OGP.GetMeta(u)
25 | if err != nil {
26 | return herror.InternalServerError(err)
27 | }
28 |
29 | if res == nil {
30 | return c.JSON(http.StatusOK, model.Ogp{
31 | Type: "empty",
32 | })
33 | }
34 | return c.JSON(http.StatusOK, res)
35 | }
36 |
37 | // DeleteOgpCache DELETE /ogp/cache?url={url}
38 | func (h *Handlers) DeleteOgpCache(c echo.Context) error {
39 | u, parseErr := url.Parse(c.QueryParam(consts.ParamURL))
40 | if parseErr != nil || len(u.Scheme) == 0 || len(u.Host) == 0 {
41 | return herror.BadRequest("invalid url")
42 | }
43 |
44 | err := h.OGP.DeleteCache(u)
45 | if err != nil {
46 | return herror.InternalServerError(err)
47 | }
48 |
49 | return c.NoContent(http.StatusNoContent)
50 | }
51 |
--------------------------------------------------------------------------------
/router/v3/public_test.go:
--------------------------------------------------------------------------------
1 | package v3
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 |
9 | "github.com/traPtitech/traQ/repository"
10 | file2 "github.com/traPtitech/traQ/service/file"
11 | "github.com/traPtitech/traQ/service/rbac/role"
12 | "github.com/traPtitech/traQ/utils/random"
13 | )
14 |
15 | func TestHandlers_GetVersion(t *testing.T) {
16 | t.Parallel()
17 |
18 | path := "/api/v3/version"
19 | env := Setup(t, common1)
20 |
21 | e := env.R(t)
22 | obj := e.GET(path).
23 | Expect().
24 | Status(http.StatusOK).
25 | JSON().
26 | Object()
27 |
28 | obj.Value("version").String().IsEqual("version")
29 | obj.Value("revision").String().IsEqual("revision")
30 |
31 | flags := obj.Value("flags").Object()
32 |
33 | flags.Value("signUpAllowed").Boolean().IsFalse()
34 |
35 | ext := flags.Value("externalLogin").Array()
36 | ext.Length().IsEqual(1)
37 | ext.Value(0).String().IsEqual("traq")
38 | }
39 |
40 | func TestHandlers_GetPublicUserIcon(t *testing.T) {
41 | t.Parallel()
42 |
43 | path := "/api/v3/public/icon/{username}"
44 | env := Setup(t, common1)
45 | iconFileID, err := file2.GenerateIconFile(env.FM, "test")
46 | require.NoError(t, err)
47 | user, err := env.Repository.CreateUser(repository.CreateUserArgs{
48 | Name: random.AlphaNumeric(20),
49 | Password: "totallyASecurePassword",
50 | Role: role.User,
51 | IconFileID: iconFileID,
52 | })
53 | require.NoError(t, err)
54 |
55 | e := env.R(t)
56 | e.GET(path, user.GetName()).
57 | Expect().
58 | Status(http.StatusOK).
59 | HasContentType("image/png")
60 | }
61 |
--------------------------------------------------------------------------------
/router/v3/user_settings.go:
--------------------------------------------------------------------------------
1 | package v3
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 |
8 | "github.com/traPtitech/traQ/router/extension/herror"
9 | )
10 |
11 | // PutMyNotifyCitationRequest PUT /user/me/settings/notify-citation リクエストボディ
12 | type PutMyNotifyCitationRequest struct {
13 | NotifyCitation bool `json:"notifyCitation"`
14 | }
15 |
16 | // PutMyNotifyCitation PUT /user/me/settings/notify-citation
17 | func (h *Handlers) PutMyNotifyCitation(c echo.Context) error {
18 | id := getRequestUserID(c)
19 |
20 | var us PutMyNotifyCitationRequest
21 | if err := bindAndValidate(c, &us); err != nil {
22 | return err
23 | }
24 |
25 | if err := h.Repo.UpdateNotifyCitation(id, us.NotifyCitation); err != nil {
26 | return herror.InternalServerError(err)
27 | }
28 |
29 | return c.NoContent(http.StatusNoContent)
30 | }
31 |
32 | // GetMySettings GET /user/me/settings
33 | func (h *Handlers) GetMySettings(c echo.Context) error {
34 | id := getRequestUserID(c)
35 |
36 | us, err := h.Repo.GetUserSettings(id)
37 | if err != nil {
38 | return herror.InternalServerError(err)
39 | }
40 |
41 | return c.JSON(http.StatusOK, us)
42 | }
43 |
44 | // GetMyNotifyCitation GET /user/me/settings/notify-citation
45 | func (h *Handlers) GetMyNotifyCitation(c echo.Context) error {
46 | id := getRequestUserID(c)
47 |
48 | nc, err := h.Repo.GetNotifyCitation(id)
49 | if err != nil {
50 | return herror.InternalServerError(err)
51 | }
52 |
53 | type res struct {
54 | NotifyCitation bool `json:"notifyCitation"`
55 | }
56 |
57 | return c.JSON(http.StatusOK, &res{NotifyCitation: nc})
58 | }
59 |
--------------------------------------------------------------------------------
/router/v3/ws.go:
--------------------------------------------------------------------------------
1 | package v3
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gofrs/uuid"
7 | "github.com/labstack/echo/v4"
8 |
9 | "github.com/traPtitech/traQ/service/ws"
10 | )
11 |
12 | func (h *Handlers) GetMyViewStates(c echo.Context) error {
13 | type viewState struct {
14 | Key string `json:"key"`
15 | ChannelID uuid.UUID `json:"channelId"`
16 | State string `json:"state"`
17 | }
18 | res := make([]viewState, 0)
19 |
20 | userID := getRequestUserID(c)
21 | h.WS.IterateSessions(func(session ws.Session) {
22 | if session.UserID() == userID {
23 | channelID, state := session.ViewState()
24 | res = append(res, viewState{
25 | Key: session.Key(),
26 | ChannelID: channelID,
27 | State: state.String(),
28 | })
29 | }
30 | })
31 |
32 | return c.JSON(http.StatusOK, res)
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_bot_message_stamps_updated.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | )
10 |
11 | // BotMessageStampsUpdated BOT_MESSAGE_STAMPS_UPDATEDイベントペイロード
12 | type BotMessageStampsUpdated struct {
13 | Base
14 | MessageID uuid.UUID `json:"messageId"`
15 | Stamps []model.MessageStamp `json:"stamps"`
16 | }
17 |
18 | func MakeBotMessageStampsUpdated(eventTime time.Time, mid uuid.UUID, stamps []model.MessageStamp) *BotMessageStampsUpdated {
19 | return &BotMessageStampsUpdated{
20 | Base: MakeBase(eventTime),
21 | MessageID: mid,
22 | Stamps: stamps,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_channel_created.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // ChannelCreated CHANNEL_CREATEDイベントペイロード
10 | type ChannelCreated struct {
11 | Base
12 | Channel Channel `json:"channel"`
13 | }
14 |
15 | func MakeChannelCreated(eventTime time.Time, ch *model.Channel, chPath string, user model.UserInfo) *ChannelCreated {
16 | return &ChannelCreated{
17 | Base: MakeBase(eventTime),
18 | Channel: MakeChannel(ch, chPath, user),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_channel_topic_changed.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // ChannelTopicChanged CHANNEL_TOPIC_CHANGEDイベントペイロード
10 | type ChannelTopicChanged struct {
11 | Base
12 | Channel Channel `json:"channel"`
13 | Topic string `json:"topic"`
14 | Updater User `json:"updater"`
15 | }
16 |
17 | func MakeChannelTopicChanged(et time.Time, ch *model.Channel, chPath string, chCreator model.UserInfo, topic string, user model.UserInfo) *ChannelTopicChanged {
18 | return &ChannelTopicChanged{
19 | Base: MakeBase(et),
20 | Channel: MakeChannel(ch, chPath, chCreator),
21 | Topic: topic,
22 | Updater: MakeUser(user),
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_direct_message_created.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | "github.com/traPtitech/traQ/utils/message"
8 | )
9 |
10 | // DirectMessageCreated DIRECT_MESSAGE_CREATEDイベントペイロード
11 | type DirectMessageCreated struct {
12 | Base
13 | Message Message `json:"message"`
14 | }
15 |
16 | func MakeDirectMessageCreated(et time.Time, m *model.Message, user model.UserInfo, parsed *message.ParseResult) *DirectMessageCreated {
17 | embedded, _ := message.ExtractEmbedding(m.Text)
18 | return &DirectMessageCreated{
19 | Base: MakeBase(et),
20 | Message: MakeMessage(m, user, embedded, parsed.PlainText),
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_direct_message_deleted.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | )
10 |
11 | // DirectMessageDeleted DIRECT_MESSAGE_DELETEDイベントペイロード
12 | type DirectMessageDeleted struct {
13 | Base
14 | Message struct {
15 | ID uuid.UUID `json:"id"`
16 | UserID uuid.UUID `json:"userId"`
17 | ChannelID uuid.UUID `json:"channelId"`
18 | } `json:"message"`
19 | }
20 |
21 | func MakeDirectMessageDeleted(et time.Time, m *model.Message) *DirectMessageDeleted {
22 | return &DirectMessageDeleted{
23 | Base: MakeBase(et),
24 | Message: struct {
25 | ID uuid.UUID `json:"id"`
26 | UserID uuid.UUID `json:"userId"`
27 | ChannelID uuid.UUID `json:"channelId"`
28 | }{
29 | ID: m.ID,
30 | UserID: m.UserID,
31 | ChannelID: m.ChannelID,
32 | },
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_direct_message_updated.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | "github.com/traPtitech/traQ/utils/message"
8 | )
9 |
10 | // DirectMessageUpdated DIRECT_MESSAGE_UPDATEDイベントペイロード
11 | type DirectMessageUpdated struct {
12 | Base
13 | Message Message `json:"message"`
14 | }
15 |
16 | func MakeDirectMessageUpdated(et time.Time, m *model.Message, user model.UserInfo, parsed *message.ParseResult) *DirectMessageUpdated {
17 | embedded, _ := message.ExtractEmbedding(m.Text)
18 | return &DirectMessageUpdated{
19 | Base: MakeBase(et),
20 | Message: MakeMessage(m, user, embedded, parsed.PlainText),
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_joined.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // Joined JOINEDイベントペイロード
10 | type Joined struct {
11 | Base
12 | Channel Channel `json:"channel"`
13 | }
14 |
15 | func MakeJoined(et time.Time, ch *model.Channel, chPath string, user model.UserInfo) *Joined {
16 | return &Joined{
17 | Base: MakeBase(et),
18 | Channel: MakeChannel(ch, chPath, user),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_left.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // Left LEFTイベントペイロード
10 | type Left struct {
11 | Base
12 | Channel Channel `json:"channel"`
13 | }
14 |
15 | func MakeLeft(et time.Time, ch *model.Channel, chPath string, user model.UserInfo) *Left {
16 | return &Left{
17 | Base: MakeBase(et),
18 | Channel: MakeChannel(ch, chPath, user),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_message_created.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | "github.com/traPtitech/traQ/utils/message"
8 | )
9 |
10 | // MessageCreated MESSAGE_CREATEDイベントペイロード
11 | type MessageCreated struct {
12 | Base
13 | Message Message `json:"message"`
14 | }
15 |
16 | func MakeMessageCreated(et time.Time, m *model.Message, user model.UserInfo, parsed *message.ParseResult) *MessageCreated {
17 | embedded, _ := message.ExtractEmbedding(m.Text)
18 | return &MessageCreated{
19 | Base: MakeBase(et),
20 | Message: MakeMessage(m, user, embedded, parsed.PlainText),
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_message_deleted.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | )
10 |
11 | // MessageDeleted MESSAGE_DELETEDイベントペイロード
12 | type MessageDeleted struct {
13 | Base
14 | Message struct {
15 | ID uuid.UUID `json:"id"`
16 | ChannelID uuid.UUID `json:"channelId"`
17 | } `json:"message"`
18 | }
19 |
20 | func MakeMessageDeleted(et time.Time, m *model.Message) *MessageDeleted {
21 | return &MessageDeleted{
22 | Base: MakeBase(et),
23 | Message: struct {
24 | ID uuid.UUID `json:"id"`
25 | ChannelID uuid.UUID `json:"channelId"`
26 | }{
27 | ID: m.ID,
28 | ChannelID: m.ChannelID,
29 | },
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_message_updated.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | "github.com/traPtitech/traQ/utils/message"
8 | )
9 |
10 | // MessageUpdated MESSAGE_UPDATEDイベントペイロード
11 | type MessageUpdated struct {
12 | Base
13 | Message Message `json:"message"`
14 | }
15 |
16 | func MakeMessageUpdated(et time.Time, m *model.Message, user model.UserInfo, parsed *message.ParseResult) *MessageUpdated {
17 | embedded, _ := message.ExtractEmbedding(m.Text)
18 | return &MessageUpdated{
19 | Base: MakeBase(et),
20 | Message: MakeMessage(m, user, embedded, parsed.PlainText),
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_ping.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import "time"
4 |
5 | // Ping PINGイベントペイロード
6 | type Ping struct {
7 | Base
8 | }
9 |
10 | func MakePing(et time.Time) *Ping {
11 | return &Ping{
12 | Base: MakeBase(et),
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_stamp_created.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | )
10 |
11 | // StampCreated STAMP_CREATEDイベントペイロード
12 | type StampCreated struct {
13 | Base
14 | ID uuid.UUID `json:"id"`
15 | Name string `json:"name"`
16 | FileID uuid.UUID `json:"fileId"`
17 | Creator User `json:"creator"`
18 | }
19 |
20 | func MakeStampCreated(et time.Time, stamp *model.Stamp, user model.UserInfo) *StampCreated {
21 | return &StampCreated{
22 | Base: MakeBase(et),
23 | ID: stamp.ID,
24 | Name: stamp.Name,
25 | FileID: stamp.FileID,
26 | Creator: MakeUser(user),
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_tag_added.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | )
10 |
11 | // TagAdded TAG_ADDEDイベントペイロード
12 | type TagAdded struct {
13 | Base
14 | TagID uuid.UUID `json:"tagId"`
15 | Tag string `json:"tag"`
16 | }
17 |
18 | func MakeTagAdded(et time.Time, tag *model.Tag) *TagAdded {
19 | return &TagAdded{
20 | Base: MakeBase(et),
21 | TagID: tag.ID,
22 | Tag: tag.Name,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_tag_removed.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | )
10 |
11 | // TagRemoved TAG_REMOVEDイベントペイロード
12 | type TagRemoved struct {
13 | Base
14 | TagID uuid.UUID `json:"tagId"`
15 | Tag string `json:"tag"`
16 | }
17 |
18 | func MakeTagRemoved(et time.Time, tag *model.Tag) *TagRemoved {
19 | return &TagRemoved{
20 | Base: MakeBase(et),
21 | TagID: tag.ID,
22 | Tag: tag.Name,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_activated.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // UserActivated USER_ACTIVATEDイベントペイロード
10 | type UserActivated struct {
11 | Base
12 | User User `json:"user"`
13 | }
14 |
15 | func MakeUserActivated(et time.Time, user model.UserInfo) *UserActivated {
16 | return &UserActivated{
17 | Base: MakeBase(et),
18 | User: MakeUser(user),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_created.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // UserCreated USER_CREATEDイベントペイロード
10 | type UserCreated struct {
11 | Base
12 | User User `json:"user"`
13 | }
14 |
15 | func MakeUserCreated(et time.Time, user model.UserInfo) *UserCreated {
16 | return &UserCreated{
17 | Base: MakeBase(et),
18 | User: MakeUser(user),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_group_admin_added.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | "time"
6 | )
7 |
8 | // UserGroupAdminAdded USER_GROUP_ADMIN_ADDEDイベントペイロード
9 | type UserGroupAdminAdded struct {
10 | Base
11 | GroupMember `json:"groupMember"`
12 | }
13 |
14 | func MakeUserGroupAdminAdded(eventTime time.Time, groupID, userID uuid.UUID) *UserGroupAdminAdded {
15 | return &UserGroupAdminAdded{
16 | Base: MakeBase(eventTime),
17 | GroupMember: MakeGroupMember(groupID, userID),
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_group_admin_removed.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | "time"
6 | )
7 |
8 | // UserGroupAdminRemoved USER_GROUP_ADMIN_REMOVEDイベントペイロード
9 | type UserGroupAdminRemoved struct {
10 | Base
11 | GroupMember `json:"groupMember"`
12 | }
13 |
14 | func MakeUserGroupAdminRemoved(eventTime time.Time, groupID, userID uuid.UUID) *UserGroupAdminRemoved {
15 | return &UserGroupAdminRemoved{
16 | Base: MakeBase(eventTime),
17 | GroupMember: MakeGroupMember(groupID, userID),
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_group_created.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/traPtitech/traQ/model"
7 | )
8 |
9 | // UserGroupCreated USER_GROUP_CREATEDイベントペイロード
10 | type UserGroupCreated struct {
11 | Base
12 | Group UserGroup `json:"group"`
13 | }
14 |
15 | func MakeUserGroupCreated(eventTime time.Time, group *model.UserGroup) *UserGroupCreated {
16 | return &UserGroupCreated{
17 | Base: MakeBase(eventTime),
18 | Group: MakeUserGroup(group),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_group_deleted.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | )
8 |
9 | // UserGroupDeleted USER_GROUP_DELETEDイベントペイロード
10 | type UserGroupDeleted struct {
11 | Base
12 | GroupID uuid.UUID `json:"groupId"`
13 | }
14 |
15 | func MakeUserGroupDeleted(eventTime time.Time, groupID uuid.UUID) *UserGroupDeleted {
16 | return &UserGroupDeleted{
17 | Base: MakeBase(eventTime),
18 | GroupID: groupID,
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_group_member_added.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | "time"
6 | )
7 |
8 | // UserGroupMemberAdded USER_GROUP_MEMBER_ADDEDイベントペイロード
9 | type UserGroupMemberAdded struct {
10 | Base
11 | GroupMember `json:"groupMember"`
12 | }
13 |
14 | func MakeUserGroupMemberAdded(eventTime time.Time, groupID, userID uuid.UUID) *UserGroupMemberAdded {
15 | return &UserGroupMemberAdded{
16 | Base: MakeBase(eventTime),
17 | GroupMember: MakeGroupMember(groupID, userID),
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_group_member_removed.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | "time"
6 | )
7 |
8 | // UserGroupMemberRemoved USER_GROUP_MEMBER_REMOVEDイベントペイロード
9 | type UserGroupMemberRemoved struct {
10 | Base
11 | GroupMember `json:"groupMember"`
12 | }
13 |
14 | func MakeUserGroupMemberRemoved(eventTime time.Time, groupID, userID uuid.UUID) *UserGroupMemberRemoved {
15 | return &UserGroupMemberRemoved{
16 | Base: MakeBase(eventTime),
17 | GroupMember: MakeGroupMember(groupID, userID),
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_group_member_updated.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | "time"
6 | )
7 |
8 | // UserGroupMemberUpdated USER_GROUP_MEMBER_UPDATEDイベントペイロード
9 | type UserGroupMemberUpdated struct {
10 | Base
11 | GroupMember `json:"groupMember"`
12 | }
13 |
14 | func MakeUserGroupMemberUpdated(eventTime time.Time, groupID, userID uuid.UUID) *UserGroupMemberUpdated {
15 | return &UserGroupMemberUpdated{
16 | Base: MakeBase(eventTime),
17 | GroupMember: MakeGroupMember(groupID, userID),
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/service/bot/event/payload/ev_user_group_updated.go:
--------------------------------------------------------------------------------
1 | package payload
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | )
8 |
9 | // UserGroupUpdated USER_GROUP_UPDATEDイベントペイロード
10 | type UserGroupUpdated struct {
11 | Base
12 | GroupID uuid.UUID `json:"groupId"`
13 | }
14 |
15 | func MakeUserGroupUpdated(eventTime time.Time, groupID uuid.UUID) *UserGroupUpdated {
16 | return &UserGroupUpdated{
17 | Base: MakeBase(eventTime),
18 | GroupID: groupID,
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/service/bot/handler/context.go:
--------------------------------------------------------------------------------
1 | //go:generate mockgen -source=$GOFILE -destination=mock_$GOPACKAGE/mock_$GOFILE
2 | package handler
3 |
4 | import (
5 | "github.com/gofrs/uuid"
6 | "go.uber.org/zap"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | "github.com/traPtitech/traQ/repository"
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/channel"
12 | )
13 |
14 | type Context interface {
15 | CM() channel.Manager
16 | R() repository.Repository
17 | L() *zap.Logger
18 | D() event.Dispatcher
19 |
20 | Unicast(ev model.BotEventType, payload interface{}, target *model.Bot) error
21 | Multicast(ev model.BotEventType, payload interface{}, targets []*model.Bot) error
22 |
23 | GetBot(id uuid.UUID) (*model.Bot, error)
24 | GetBotByBotUserID(uid uuid.UUID) (*model.Bot, error)
25 | GetBots(event model.BotEventType) ([]*model.Bot, error)
26 | GetChannelBots(cid uuid.UUID, event model.BotEventType) ([]*model.Bot, error)
27 | }
28 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_bot_joined.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/repository"
11 | "github.com/traPtitech/traQ/service/bot/event"
12 | "github.com/traPtitech/traQ/service/bot/event/payload"
13 | )
14 |
15 | func BotJoined(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
16 | botID := fields["bot_id"].(uuid.UUID)
17 | channelID := fields["channel_id"].(uuid.UUID)
18 |
19 | bot, err := ctx.GetBot(botID)
20 | if err != nil {
21 | return fmt.Errorf("failed to GetBot: %w", err)
22 | }
23 |
24 | ch, err := ctx.CM().GetChannel(channelID)
25 | if err != nil {
26 | return fmt.Errorf("failed to GetChannel: %w", err)
27 | }
28 | user, err := ctx.R().GetUser(ch.CreatorID, false)
29 | if err != nil && err != repository.ErrNotFound {
30 | return fmt.Errorf("failed to GetUser: %w", err)
31 | }
32 |
33 | err = ctx.Unicast(
34 | event.Joined,
35 | payload.MakeJoined(datetime, ch, ctx.CM().PublicChannelTree().GetChannelPath(channelID), user),
36 | bot,
37 | )
38 | if err != nil {
39 | return fmt.Errorf("failed to unicast: %w", err)
40 | }
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_bot_left.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/repository"
11 | "github.com/traPtitech/traQ/service/bot/event"
12 | "github.com/traPtitech/traQ/service/bot/event/payload"
13 | )
14 |
15 | func BotLeft(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
16 | botID := fields["bot_id"].(uuid.UUID)
17 | channelID := fields["channel_id"].(uuid.UUID)
18 |
19 | bot, err := ctx.GetBot(botID)
20 | if err != nil {
21 | return fmt.Errorf("failed to GetBot: %w", err)
22 | }
23 |
24 | ch, err := ctx.CM().GetChannel(channelID)
25 | if err != nil {
26 | return fmt.Errorf("failed to GetChannel: %w", err)
27 | }
28 | user, err := ctx.R().GetUser(ch.CreatorID, false)
29 | if err != nil && err != repository.ErrNotFound {
30 | return fmt.Errorf("failed to GetUser: %w", err)
31 | }
32 |
33 | err = ctx.Unicast(
34 | event.Left,
35 | payload.MakeLeft(datetime, ch, ctx.CM().PublicChannelTree().GetChannelPath(channelID), user),
36 | bot,
37 | )
38 | if err != nil {
39 | return fmt.Errorf("failed to unicast: %w", err)
40 | }
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_bot_ping_request.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | jsonIter "github.com/json-iterator/go"
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/model"
11 | "github.com/traPtitech/traQ/service/bot/event"
12 | "github.com/traPtitech/traQ/service/bot/event/payload"
13 | )
14 |
15 | func BotPingRequest(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
16 | bot := fields["bot"].(*model.Bot)
17 |
18 | buf, err := jsonIter.ConfigFastest.Marshal(payload.MakePing(datetime))
19 | if err != nil {
20 | return err
21 | }
22 |
23 | if ctx.D().Send(bot, event.Ping, buf) {
24 | // OK
25 | if err := ctx.R().ChangeBotState(bot.ID, model.BotActive); err != nil {
26 | return fmt.Errorf("failed to ChangeBotState: %w", err)
27 | }
28 | } else {
29 | // NG
30 | if err := ctx.R().ChangeBotState(bot.ID, model.BotPaused); err != nil {
31 | return fmt.Errorf("failed to ChangeBotState: %w", err)
32 | }
33 | }
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_channel_created.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/leandro-lugaresi/hub"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func ChannelCreated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | ch := fields["channel"].(*model.Channel)
16 | if ch.IsPublic {
17 | bots, err := ctx.GetBots(event.ChannelCreated)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | user, err := ctx.R().GetUser(ch.CreatorID, false)
26 | if err != nil {
27 | return fmt.Errorf("failed to GetUser: %w", err)
28 | }
29 |
30 | if err := ctx.Multicast(
31 | event.ChannelCreated,
32 | payload.MakeChannelCreated(datetime, ch, ctx.CM().PublicChannelTree().GetChannelPath(ch.ID), user),
33 | bots,
34 | ); err != nil {
35 | return fmt.Errorf("failed to multicast: %w", err)
36 | }
37 | }
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_channel_topic_updated.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/repository"
11 | "github.com/traPtitech/traQ/service/bot/event"
12 | "github.com/traPtitech/traQ/service/bot/event/payload"
13 | )
14 |
15 | func ChannelTopicUpdated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
16 | chID := fields["channel_id"].(uuid.UUID)
17 | topic := fields["topic"].(string)
18 | updaterID := fields["updater_id"].(uuid.UUID)
19 |
20 | bots, err := ctx.GetChannelBots(chID, event.ChannelTopicChanged)
21 | if err != nil {
22 | return fmt.Errorf("failed to GetChannelBots: %w", err)
23 | }
24 | if len(bots) == 0 {
25 | return nil
26 | }
27 |
28 | ch, err := ctx.CM().GetChannel(chID)
29 | if err != nil {
30 | return fmt.Errorf("failed to GetChannel: %w", err)
31 | }
32 |
33 | chCreator, err := ctx.R().GetUser(ch.CreatorID, false)
34 | if err != nil && err != repository.ErrNotFound {
35 | return fmt.Errorf("failed to GetUser: %w", err)
36 | }
37 |
38 | user, err := ctx.R().GetUser(updaterID, false)
39 | if err != nil {
40 | return fmt.Errorf("failed to GetUser: %w", err)
41 | }
42 |
43 | if err := ctx.Multicast(
44 | event.ChannelTopicChanged,
45 | payload.MakeChannelTopicChanged(datetime, ch, ctx.CM().PublicChannelTree().GetChannelPath(ch.ID), chCreator, topic, user),
46 | bots,
47 | ); err != nil {
48 | return fmt.Errorf("failed to multicast: %w", err)
49 | }
50 | return nil
51 | }
52 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_message_stamps_updated.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/leandro-lugaresi/hub"
8 |
9 | "github.com/traPtitech/traQ/service/bot/event"
10 | "github.com/traPtitech/traQ/service/bot/event/payload"
11 | "github.com/traPtitech/traQ/service/message"
12 | )
13 |
14 | func MessageStampsUpdated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | m := fields["message"].(message.Message)
16 |
17 | bot, err := ctx.GetBotByBotUserID(m.GetUserID())
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBotByBotUserID: %w", err)
20 | }
21 | if bot == nil || !bot.SubscribeEvents.Contains(event.BotMessageStampsUpdated) {
22 | return nil
23 | }
24 |
25 | if err := ctx.Unicast(
26 | event.BotMessageStampsUpdated,
27 | payload.MakeBotMessageStampsUpdated(datetime, m.GetID(), m.GetStamps()),
28 | bot,
29 | ); err != nil {
30 | return fmt.Errorf("failed to unicast: %w", err)
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_stamp_created.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/leandro-lugaresi/hub"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func StampCreated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | stamp := fields["stamp"].(*model.Stamp)
16 |
17 | bots, err := ctx.GetBots(event.StampCreated)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | var user model.UserInfo
26 | if !stamp.IsSystemStamp() {
27 | user, err = ctx.R().GetUser(stamp.CreatorID, false)
28 | if err != nil {
29 | return fmt.Errorf("failed to GetUser: %w", err)
30 | }
31 | }
32 |
33 | if err := ctx.Multicast(
34 | event.StampCreated,
35 | payload.MakeStampCreated(datetime, stamp, user),
36 | bots,
37 | ); err != nil {
38 | return fmt.Errorf("failed to multicast: %w", err)
39 | }
40 | return nil
41 | }
42 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_activated.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/leandro-lugaresi/hub"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserActivated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | user := fields["user"].(model.UserInfo)
16 |
17 | bots, err := ctx.GetBots(event.UserActivated)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | if err := ctx.Multicast(
26 | event.UserActivated,
27 | payload.MakeUserActivated(datetime, user),
28 | bots,
29 | ); err != nil {
30 | return fmt.Errorf("failed to multicast: %w", err)
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_activated_test.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/golang/mock/gomock"
9 | "github.com/leandro-lugaresi/hub"
10 | "github.com/stretchr/testify/assert"
11 |
12 | intevent "github.com/traPtitech/traQ/event"
13 | "github.com/traPtitech/traQ/model"
14 | "github.com/traPtitech/traQ/service/bot/event"
15 | "github.com/traPtitech/traQ/service/bot/event/payload"
16 | "github.com/traPtitech/traQ/service/bot/handler/mock_handler"
17 | )
18 |
19 | func TestUserActivated(t *testing.T) {
20 | t.Parallel()
21 |
22 | b := &model.Bot{
23 | ID: uuid.NewV3(uuid.Nil, "b"),
24 | BotUserID: uuid.NewV3(uuid.Nil, "bu"),
25 | SubscribeEvents: model.BotEventTypesFromArray([]string{event.UserActivated.String()}),
26 | State: model.BotActive,
27 | }
28 |
29 | t.Run("success", func(t *testing.T) {
30 | t.Parallel()
31 | ctrl := gomock.NewController(t)
32 | handlerCtx := mock_handler.NewMockContext(ctrl)
33 | registerBot(t, handlerCtx, b)
34 |
35 | user := &model.User{
36 | ID: uuid.NewV3(uuid.Nil, "u"),
37 | Name: "activated_user",
38 | Status: model.UserAccountStatusActive,
39 | Bot: false,
40 | }
41 | et := time.Now()
42 |
43 | expectMulticast(handlerCtx, event.UserActivated, payload.MakeUserActivated(et, user), []*model.Bot{b})
44 | assert.NoError(t, UserActivated(handlerCtx, et, intevent.UserActivated, hub.Fields{
45 | "user": user,
46 | }))
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_created.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/leandro-lugaresi/hub"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserCreated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | user := fields["user"].(model.UserInfo)
16 |
17 | bots, err := ctx.GetBots(event.UserCreated)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | if err := ctx.Multicast(
26 | event.UserCreated,
27 | payload.MakeUserCreated(datetime, user),
28 | bots,
29 | ); err != nil {
30 | return fmt.Errorf("failed to multicast: %w", err)
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_created_test.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/golang/mock/gomock"
9 | "github.com/leandro-lugaresi/hub"
10 | "github.com/stretchr/testify/assert"
11 |
12 | intevent "github.com/traPtitech/traQ/event"
13 | "github.com/traPtitech/traQ/model"
14 | "github.com/traPtitech/traQ/service/bot/event"
15 | "github.com/traPtitech/traQ/service/bot/event/payload"
16 | "github.com/traPtitech/traQ/service/bot/handler/mock_handler"
17 | )
18 |
19 | func TestUserCreated(t *testing.T) {
20 | t.Parallel()
21 |
22 | b := &model.Bot{
23 | ID: uuid.NewV3(uuid.Nil, "b"),
24 | BotUserID: uuid.NewV3(uuid.Nil, "bu"),
25 | SubscribeEvents: model.BotEventTypesFromArray([]string{event.UserCreated.String()}),
26 | State: model.BotActive,
27 | }
28 |
29 | t.Run("success", func(t *testing.T) {
30 | t.Parallel()
31 | ctrl := gomock.NewController(t)
32 | handlerCtx := mock_handler.NewMockContext(ctrl)
33 | registerBot(t, handlerCtx, b)
34 |
35 | user := &model.User{
36 | ID: uuid.NewV3(uuid.Nil, "u"),
37 | Name: "new_user",
38 | Status: model.UserAccountStatusActive,
39 | Bot: false,
40 | }
41 | et := time.Now()
42 |
43 | expectMulticast(handlerCtx, event.UserCreated, payload.MakeUserCreated(et, user), []*model.Bot{b})
44 | assert.NoError(t, UserCreated(handlerCtx, et, intevent.UserCreated, hub.Fields{
45 | "user_id": user.ID,
46 | "user": user,
47 | }))
48 | })
49 | }
50 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_group_admin_added.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "github.com/gofrs/uuid"
6 | "time"
7 |
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserGroupAdminAdded(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | groupID := fields["group_id"].(uuid.UUID)
16 | userID := fields["user_id"].(uuid.UUID)
17 | bots, err := ctx.GetBots(event.UserGroupAdminAdded)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | if err := ctx.Multicast(
26 | event.UserGroupAdminAdded,
27 | payload.MakeUserGroupAdminAdded(datetime, groupID, userID),
28 | bots,
29 | ); err != nil {
30 | return fmt.Errorf("failed to multicast: %w", err)
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_group_admin_removed.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "github.com/gofrs/uuid"
6 | "time"
7 |
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserGroupAdminRemoved(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | groupID := fields["group_id"].(uuid.UUID)
16 | userID := fields["user_id"].(uuid.UUID)
17 | bots, err := ctx.GetBots(event.UserGroupAdminRemoved)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | if err := ctx.Multicast(
26 | event.UserGroupAdminRemoved,
27 | payload.MakeUserGroupAdminRemoved(datetime, groupID, userID),
28 | bots,
29 | ); err != nil {
30 | return fmt.Errorf("failed to multicast: %w", err)
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_group_created.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/leandro-lugaresi/hub"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserGroupCreated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | group := fields["group"].(*model.UserGroup)
16 | bots, err := ctx.GetBots(event.UserGroupCreated)
17 | if err != nil {
18 | return fmt.Errorf("failed to GetBots: %w", err)
19 | }
20 | if len(bots) == 0 {
21 | return nil
22 | }
23 |
24 | if err := ctx.Multicast(
25 | event.UserGroupCreated,
26 | payload.MakeUserGroupCreated(datetime, group),
27 | bots,
28 | ); err != nil {
29 | return fmt.Errorf("failed to multicast: %w", err)
30 | }
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_group_deleted.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserGroupDeleted(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | groupID := fields["group_id"].(uuid.UUID)
16 | bots, err := ctx.GetBots(event.UserGroupDeleted)
17 | if err != nil {
18 | return fmt.Errorf("failed to GetBots: %w", err)
19 | }
20 | if len(bots) == 0 {
21 | return nil
22 | }
23 |
24 | if err := ctx.Multicast(
25 | event.UserGroupDeleted,
26 | payload.MakeUserGroupDeleted(datetime, groupID),
27 | bots,
28 | ); err != nil {
29 | return fmt.Errorf("failed to multicast: %w", err)
30 | }
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_group_member_added.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "github.com/gofrs/uuid"
6 | "time"
7 |
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserGroupMemberAdded(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | groupID := fields["group_id"].(uuid.UUID)
16 | userID := fields["user_id"].(uuid.UUID)
17 | bots, err := ctx.GetBots(event.UserGroupMemberAdded)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | if err := ctx.Multicast(
26 | event.UserGroupMemberAdded,
27 | payload.MakeUserGroupMemberAdded(datetime, groupID, userID),
28 | bots,
29 | ); err != nil {
30 | return fmt.Errorf("failed to multicast: %w", err)
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_group_member_removed.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "github.com/gofrs/uuid"
6 | "time"
7 |
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserGroupMemberRemoved(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | groupID := fields["group_id"].(uuid.UUID)
16 | userID := fields["user_id"].(uuid.UUID)
17 | bots, err := ctx.GetBots(event.UserGroupMemberRemoved)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | if err := ctx.Multicast(
26 | event.UserGroupMemberRemoved,
27 | payload.MakeUserGroupMemberRemoved(datetime, groupID, userID),
28 | bots,
29 | ); err != nil {
30 | return fmt.Errorf("failed to multicast: %w", err)
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_group_member_updated.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "github.com/gofrs/uuid"
6 | "time"
7 |
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserGroupMemberUpdated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | groupID := fields["group_id"].(uuid.UUID)
16 | userID := fields["user_id"].(uuid.UUID)
17 | bots, err := ctx.GetBots(event.UserGroupMemberUpdated)
18 | if err != nil {
19 | return fmt.Errorf("failed to GetBots: %w", err)
20 | }
21 | if len(bots) == 0 {
22 | return nil
23 | }
24 |
25 | if err := ctx.Multicast(
26 | event.UserGroupMemberUpdated,
27 | payload.MakeUserGroupMemberUpdated(datetime, groupID, userID),
28 | bots,
29 | ); err != nil {
30 | return fmt.Errorf("failed to multicast: %w", err)
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_group_updated.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserGroupUpdated(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | groupID := fields["group_id"].(uuid.UUID)
16 | bots, err := ctx.GetBots(event.UserGroupUpdated)
17 | if err != nil {
18 | return fmt.Errorf("failed to GetBots: %w", err)
19 | }
20 | if len(bots) == 0 {
21 | return nil
22 | }
23 |
24 | if err := ctx.Multicast(
25 | event.UserGroupUpdated,
26 | payload.MakeUserGroupUpdated(datetime, groupID),
27 | bots,
28 | ); err != nil {
29 | return fmt.Errorf("failed to multicast: %w", err)
30 | }
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_tag_added.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserTagAdded(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | userID := fields["user_id"].(uuid.UUID)
16 | tagID := fields["tag_id"].(uuid.UUID)
17 |
18 | bot, err := ctx.GetBotByBotUserID(userID)
19 | if err != nil {
20 | return fmt.Errorf("failed to GetBotByBotUserID: %w", err)
21 | }
22 | if bot == nil || !bot.SubscribeEvents.Contains(event.TagAdded) {
23 | return nil
24 | }
25 |
26 | t, err := ctx.R().GetTagByID(tagID)
27 | if err != nil {
28 | return fmt.Errorf("failed to GetTagByID: %w", err)
29 | }
30 |
31 | if err := ctx.Unicast(
32 | event.TagAdded,
33 | payload.MakeTagAdded(datetime, t),
34 | bot,
35 | ); err != nil {
36 | return fmt.Errorf("failed to unicast: %w", err)
37 | }
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/service/bot/handler/ev_user_tag_removed.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | "github.com/leandro-lugaresi/hub"
9 |
10 | "github.com/traPtitech/traQ/service/bot/event"
11 | "github.com/traPtitech/traQ/service/bot/event/payload"
12 | )
13 |
14 | func UserTagRemoved(ctx Context, datetime time.Time, _ string, fields hub.Fields) error {
15 | userID := fields["user_id"].(uuid.UUID)
16 | tagID := fields["tag_id"].(uuid.UUID)
17 |
18 | bot, err := ctx.GetBotByBotUserID(userID)
19 | if err != nil {
20 | return fmt.Errorf("failed to GetBotByBotUserID: %w", err)
21 | }
22 | if bot == nil || !bot.SubscribeEvents.Contains(event.TagRemoved) {
23 | return nil
24 | }
25 |
26 | t, err := ctx.R().GetTagByID(tagID)
27 | if err != nil {
28 | return fmt.Errorf("failed to GetTagByID: %w", err)
29 | }
30 |
31 | if err := ctx.Unicast(
32 | event.TagRemoved,
33 | payload.MakeTagRemoved(datetime, t),
34 | bot,
35 | ); err != nil {
36 | return fmt.Errorf("failed to unicast: %w", err)
37 | }
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/service/bot/service.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import "context"
4 |
5 | // Service BOTサービス
6 | type Service interface {
7 | // Shutdown BOTサービスをシャットダウンします
8 | Shutdown(ctx context.Context) error
9 | }
10 |
--------------------------------------------------------------------------------
/service/bot/ws/config.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/gorilla/websocket"
8 | jsonIter "github.com/json-iterator/go"
9 | )
10 |
11 | const (
12 | writeWait = 5 * time.Second
13 | pongWait = 60 * time.Second
14 | pingPeriod = (pongWait * 9) / 10
15 | maxReadMessageSize = 1 << 9 // 512B
16 | messageBufferSize = 256
17 | )
18 |
19 | var (
20 | json = jsonIter.ConfigFastest
21 | upgrader = &websocket.Upgrader{
22 | ReadBufferSize: 1024,
23 | WriteBufferSize: 1024,
24 | CheckOrigin: func(_ *http.Request) bool { return true },
25 | }
26 | )
27 |
--------------------------------------------------------------------------------
/service/bot/ws/message.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | import "github.com/gofrs/uuid"
4 |
5 | type rawMessage struct {
6 | t int
7 | data []byte
8 | }
9 |
10 | type marshalledRaw []byte
11 |
12 | func (m marshalledRaw) MarshalJSON() ([]byte, error) {
13 | return m, nil
14 | }
15 |
16 | type eventMessage struct {
17 | Type string `json:"type"`
18 | ReqID uuid.UUID `json:"reqId"`
19 | Body marshalledRaw `json:"body"`
20 | }
21 |
22 | func makeEventMessage(t string, reqID uuid.UUID, b []byte) (m *eventMessage) {
23 | return &eventMessage{
24 | Type: t,
25 | ReqID: reqID,
26 | Body: b,
27 | }
28 | }
29 |
30 | func (m *eventMessage) toJSON() (b []byte) {
31 | b, _ = json.Marshal(m)
32 | return
33 | }
34 |
35 | type errorMessage struct {
36 | Type string `json:"type"`
37 | Body interface{} `json:"body"`
38 | }
39 |
40 | func makeErrorMessage(b interface{}) (m *errorMessage) {
41 | return &errorMessage{
42 | Type: "ERROR",
43 | Body: b,
44 | }
45 | }
46 |
47 | func (m *errorMessage) toJSON() (b []byte) {
48 | b, _ = json.Marshal(m)
49 | return
50 | }
51 |
--------------------------------------------------------------------------------
/service/bot/ws/metrics.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | import (
4 | "github.com/gofrs/uuid"
5 | "github.com/prometheus/client_golang/prometheus"
6 | "github.com/prometheus/client_golang/prometheus/promauto"
7 | )
8 |
9 | var (
10 | webSocketReadBytesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
11 | Namespace: "traq",
12 | Name: "bot_ws_read_bytes_total",
13 | }, []string{"user_id"})
14 |
15 | webSocketWriteBytesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
16 | Namespace: "traq",
17 | Name: "bot_ws_write_bytes_total",
18 | }, []string{"user_id"})
19 | )
20 |
21 | func incWebSocketReadBytesTotal(userID uuid.UUID, bytes int) {
22 | webSocketReadBytesTotal.WithLabelValues(userID.String()).Add(float64(bytes))
23 | }
24 |
25 | func incWebSocketWriteBytesTotal(userID uuid.UUID, bytes int) {
26 | webSocketWriteBytesTotal.WithLabelValues(userID.String()).Add(float64(bytes))
27 | }
28 |
--------------------------------------------------------------------------------
/service/channel/tree.go:
--------------------------------------------------------------------------------
1 | //go:generate mockgen -source=$GOFILE -destination=mock_$GOPACKAGE/mock_$GOFILE
2 | package channel
3 |
4 | import (
5 | "encoding/json"
6 |
7 | "github.com/gofrs/uuid"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | )
11 |
12 | // Tree 公開チャンネルのチャンネル階層木
13 | type Tree interface {
14 | // GetModel 指定したチャンネルの*model.Channelを取得する
15 | GetModel(id uuid.UUID) (*model.Channel, error)
16 | // GetChildrenIDs 子チャンネルのIDの配列を取得する
17 | GetChildrenIDs(id uuid.UUID) []uuid.UUID
18 | // GetDescendantIDs 子孫チャンネルのIDの配列を取得する
19 | GetDescendantIDs(id uuid.UUID) []uuid.UUID
20 | // GetAscendantIDs 祖先チャンネルのIDの配列を取得する
21 | GetAscendantIDs(id uuid.UUID) []uuid.UUID
22 | // GetChannelDepth 指定したチャンネル木の深さを取得する
23 | GetChannelDepth(id uuid.UUID) int
24 | // IsChildPresent 指定したnameのチャンネルが指定したチャンネルの子に存在するか
25 | IsChildPresent(name string, parent uuid.UUID) bool
26 | // GetChannelPath 指定したチャンネルのパスを取得する
27 | GetChannelPath(id uuid.UUID) string
28 | // IsChannelPresent 指定したIDのチャンネルが存在するかどうかを取得する
29 | IsChannelPresent(id uuid.UUID) bool
30 | // GetChannelIDFromPath チャンネルパスからチャンネルIDを取得する
31 | GetChannelIDFromPath(path string) uuid.UUID
32 | // IsForceChannel 指定したチャンネルが強制通知チャンネルかどうか
33 | IsForceChannel(id uuid.UUID) bool
34 | // IsArchivedChannel 指定したチャンネルがアーカイブされているかどうか
35 | IsArchivedChannel(id uuid.UUID) bool
36 | json.Marshaler
37 | }
38 |
--------------------------------------------------------------------------------
/service/counter/channel.go:
--------------------------------------------------------------------------------
1 | package counter
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | "github.com/leandro-lugaresi/hub"
8 | "github.com/prometheus/client_golang/prometheus"
9 | "github.com/prometheus/client_golang/prometheus/promauto"
10 | "gorm.io/gorm"
11 |
12 | "github.com/traPtitech/traQ/event"
13 | "github.com/traPtitech/traQ/model"
14 | )
15 |
16 | var channelsCounter = promauto.NewCounter(prometheus.CounterOpts{
17 | Namespace: "traq",
18 | Name: "channels_count_total",
19 | })
20 |
21 | // ChannelCounter 公開チャンネル数カウンタ
22 | type ChannelCounter interface {
23 | // Get 公開チャンネル数を返します
24 | Get() int64
25 | }
26 |
27 | type channelCounterImpl struct {
28 | count int64
29 | sync.RWMutex
30 | }
31 |
32 | // NewChannelCounter 公開チャンネル数カウンタを生成します
33 | func NewChannelCounter(db *gorm.DB, hub *hub.Hub) (ChannelCounter, error) {
34 | counter := &channelCounterImpl{}
35 | if err := db.Unscoped().Model(&model.Channel{}).Where(&model.Channel{IsPublic: true}).Count(&counter.count).Error; err != nil {
36 | return nil, fmt.Errorf("failed to load public channels count: %w", err)
37 | }
38 | channelsCounter.Add(float64(counter.count))
39 | go func() {
40 | for e := range hub.Subscribe(1, event.ChannelCreated).Receiver {
41 | if e.Fields["channel"].(*model.Channel).IsPublic {
42 | counter.inc()
43 | }
44 | }
45 | }()
46 | return counter, nil
47 | }
48 |
49 | func (c *channelCounterImpl) Get() int64 {
50 | c.RLock()
51 | defer c.RUnlock()
52 | return c.count
53 | }
54 |
55 | func (c *channelCounterImpl) inc() {
56 | c.Lock()
57 | c.count++
58 | c.Unlock()
59 | channelsCounter.Inc()
60 | }
61 |
--------------------------------------------------------------------------------
/service/counter/message.go:
--------------------------------------------------------------------------------
1 | package counter
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | "github.com/leandro-lugaresi/hub"
8 | "github.com/prometheus/client_golang/prometheus"
9 | "github.com/prometheus/client_golang/prometheus/promauto"
10 | "gorm.io/gorm"
11 |
12 | "github.com/traPtitech/traQ/event"
13 | "github.com/traPtitech/traQ/model"
14 | )
15 |
16 | var messagesCounter = promauto.NewCounter(prometheus.CounterOpts{
17 | Namespace: "traq",
18 | Name: "messages_count_total",
19 | })
20 |
21 | // MessageCounter 全メッセージ数カウンタ
22 | type MessageCounter interface {
23 | // Get 全メッセージ数を返します
24 | //
25 | // この数値は削除されたメッセージを含んでいます
26 | Get() int64
27 | }
28 |
29 | type messageCounterImpl struct {
30 | count int64
31 | sync.RWMutex
32 | }
33 |
34 | // NewMessageCounter 全メッセージ数カウンタを生成します
35 | func NewMessageCounter(db *gorm.DB, hub *hub.Hub) (MessageCounter, error) {
36 | counter := &messageCounterImpl{}
37 | if err := db.Unscoped().Model(&model.Message{}).Count(&counter.count).Error; err != nil {
38 | return nil, fmt.Errorf("failed to load total messages count: %w", err)
39 | }
40 | messagesCounter.Add(float64(counter.count))
41 | go func() {
42 | for range hub.Subscribe(1, event.MessageCreated).Receiver {
43 | counter.inc()
44 | }
45 | }()
46 | return counter, nil
47 | }
48 |
49 | func (c *messageCounterImpl) Get() int64 {
50 | c.RLock()
51 | defer c.RUnlock()
52 | return c.count
53 | }
54 |
55 | func (c *messageCounterImpl) inc() {
56 | c.Lock()
57 | c.count++
58 | c.Unlock()
59 | messagesCounter.Inc()
60 | }
61 |
--------------------------------------------------------------------------------
/service/exevent/stamp_throttler.go:
--------------------------------------------------------------------------------
1 | package exevent
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofrs/uuid"
7 | "github.com/leandro-lugaresi/hub"
8 |
9 | "github.com/traPtitech/traQ/event"
10 | "github.com/traPtitech/traQ/service/message"
11 | "github.com/traPtitech/traQ/utils/throttle"
12 | )
13 |
14 | const eventPublishInterval = 1 * time.Second
15 |
16 | type StampThrottler struct {
17 | bus *hub.Hub
18 | mm message.Manager
19 | throttles *throttle.Map[uuid.UUID]
20 | }
21 |
22 | func NewStampThrottler(bus *hub.Hub, mm message.Manager) *StampThrottler {
23 | st := &StampThrottler{
24 | bus: bus,
25 | mm: mm,
26 | }
27 | st.throttles = throttle.NewThrottleMap(eventPublishInterval, 5*time.Second, st.publishMessageStampsUpdated)
28 |
29 | return st
30 | }
31 |
32 | func (st *StampThrottler) Start() {
33 | go st.run()
34 | }
35 |
36 | func (st *StampThrottler) run() {
37 | sub := st.bus.Subscribe(100, event.MessageStamped, event.MessageUnstamped)
38 | defer st.bus.Unsubscribe(sub)
39 |
40 | for msg := range sub.Receiver {
41 | messageID, ok := msg.Fields["message_id"].(uuid.UUID)
42 | if !ok {
43 | continue // ignore invalid message
44 | }
45 | st.throttles.Trigger(messageID)
46 | }
47 | }
48 |
49 | func (st *StampThrottler) publishMessageStampsUpdated(messageID uuid.UUID) {
50 | msg, err := st.mm.Get(messageID)
51 | if err != nil {
52 | return // ignore error
53 | }
54 |
55 | st.bus.Publish(hub.Message{
56 | Name: event.MessageStampsUpdated,
57 | Fields: hub.Fields{
58 | "message_id": messageID,
59 | "message": msg,
60 | },
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/service/fcm/client.go:
--------------------------------------------------------------------------------
1 | package fcm
2 |
3 | import "github.com/traPtitech/traQ/utils/set"
4 |
5 | // Client Firebase Cloud Messaging Client
6 | type Client interface {
7 | // Send targetユーザーにpayloadを送信します
8 | Send(targetUserIDs set.UUID, payload *Payload, withUnreadCount bool)
9 | Close()
10 | }
11 |
--------------------------------------------------------------------------------
/service/fcm/null.go:
--------------------------------------------------------------------------------
1 | package fcm
2 |
3 | import (
4 | "github.com/traPtitech/traQ/utils/set"
5 | )
6 |
7 | var nullC = &nullClient{}
8 |
9 | type nullClient struct{}
10 |
11 | // NewNullClient 何もしないFCMクライアントを返します
12 | func NewNullClient() Client {
13 | return nullC
14 | }
15 |
16 | func (n *nullClient) Send(set.UUID, *Payload, bool) {
17 | }
18 |
19 | func (n *nullClient) Close() {
20 | }
21 |
--------------------------------------------------------------------------------
/service/file/animated_image.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/sapphi-red/midec"
7 | // add gif, png, webp support
8 | _ "github.com/sapphi-red/midec/gif"
9 | _ "github.com/sapphi-red/midec/png"
10 | _ "github.com/sapphi-red/midec/webp"
11 | )
12 |
13 | func isAnimatedImage(r io.Reader) (bool, error) {
14 | return midec.IsAnimated(r)
15 | }
16 |
--------------------------------------------------------------------------------
/service/file/utils.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "image/png"
7 |
8 | "github.com/gofrs/uuid"
9 |
10 | "github.com/traPtitech/traQ/model"
11 | "github.com/traPtitech/traQ/utils/imaging"
12 | )
13 |
14 | // GenerateIconFile アイコンファイルを生成します
15 | //
16 | // 成功した場合、そのファイルのUUIDとnilを返します。
17 | func GenerateIconFile(m Manager, salt string) (uuid.UUID, error) {
18 | var img bytes.Buffer
19 | icon, err := imaging.GenerateIcon(salt)
20 | if err != nil {
21 | return uuid.Nil, err
22 | }
23 |
24 | if err := png.Encode(&img, icon); err != nil {
25 | return uuid.Nil, err
26 | }
27 |
28 | file, err := m.Save(SaveArgs{
29 | FileName: fmt.Sprintf("%s.png", salt),
30 | FileSize: int64(img.Len()),
31 | MimeType: "image/png",
32 | FileType: model.FileTypeIcon,
33 | Src: bytes.NewReader(img.Bytes()),
34 | Thumbnail: icon,
35 | })
36 | if err != nil {
37 | return uuid.Nil, err
38 | }
39 | return file.GetID(), nil
40 | }
41 |
--------------------------------------------------------------------------------
/service/imaging/config.go:
--------------------------------------------------------------------------------
1 | package imaging
2 |
3 | import (
4 | "errors"
5 | "image"
6 | )
7 |
8 | var (
9 | ErrPixelLimitExceeded = errors.New("the image exceeds max pixels limit")
10 | ErrInvalidImageSrc = errors.New("invalid image src")
11 | )
12 |
13 | type Config struct {
14 | // MaxPixels 処理可能な最大画素数
15 | // この値を超える画素数の画像を処理しようとした場合、全てエラーになります
16 | MaxPixels int
17 | // Concurrency 処理並列数
18 | Concurrency int
19 | // ThumbnailMaxSize サムネイル画像サイズ
20 | ThumbnailMaxSize image.Point
21 | }
22 |
--------------------------------------------------------------------------------
/service/imaging/mks2013_filter.go:
--------------------------------------------------------------------------------
1 | package imaging
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/disintegration/imaging"
7 | )
8 |
9 | // Magic Kernel Sharp 2013
10 | // http://johncostella.com/magic/
11 | var mks2013Filter = imaging.ResampleFilter{
12 | Support: 2.5,
13 | Kernel: func(x float64) float64 {
14 | x = math.Abs(x)
15 | if x >= 2.5 {
16 | return 0.0
17 | }
18 | if x >= 1.5 {
19 | return -0.125 * (x - 2.5) * (x - 2.5)
20 | }
21 | if x >= 0.5 {
22 | return 0.25 * (4*x*x - 11*x + 7)
23 | }
24 | return 1.0625 - 1.75*x*x
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/service/imaging/processor.go:
--------------------------------------------------------------------------------
1 | //go:generate mockgen -source=$GOFILE -destination=mock_$GOPACKAGE/mock_$GOFILE
2 | package imaging
3 |
4 | import (
5 | "bytes"
6 | "image"
7 | "io"
8 | )
9 |
10 | type Processor interface {
11 | Thumbnail(src io.ReadSeeker) (image.Image, error)
12 | Fit(src io.ReadSeeker, width, height int) (image.Image, error)
13 | FitAnimationGIF(src io.Reader, width, height int) (*bytes.Reader, error)
14 | WaveformMp3(src io.ReadSeeker, width, height int) (io.Reader, error)
15 | WaveformWav(src io.ReadSeeker, width, height int) (io.Reader, error)
16 | }
17 |
--------------------------------------------------------------------------------
/service/message/manager_test.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "github.com/golang/mock/gomock"
5 |
6 | "github.com/traPtitech/traQ/repository/mock_repository"
7 | "github.com/traPtitech/traQ/testutils"
8 | )
9 |
10 | type Repo struct {
11 | *mock_repository.MockChannelRepository
12 | *mock_repository.MockMessageRepository
13 | *mock_repository.MockPinRepository
14 | testutils.EmptyTestRepository
15 | }
16 |
17 | func NewMockRepo(ctrl *gomock.Controller) *Repo {
18 | return &Repo{
19 | MockChannelRepository: mock_repository.NewMockChannelRepository(ctrl),
20 | MockMessageRepository: mock_repository.NewMockMessageRepository(ctrl),
21 | MockPinRepository: mock_repository.NewMockPinRepository(ctrl),
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/service/message/model.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "encoding/json"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 |
9 | "github.com/traPtitech/traQ/model"
10 | )
11 |
12 | type Message interface {
13 | GetID() uuid.UUID
14 | GetUserID() uuid.UUID
15 | GetChannelID() uuid.UUID
16 | GetText() string
17 | GetCreatedAt() time.Time
18 | GetUpdatedAt() time.Time
19 | GetStamps() []model.MessageStamp
20 | GetPin() *model.Pin
21 |
22 | json.Marshaler
23 | }
24 |
--------------------------------------------------------------------------------
/service/message/timeline.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Timeline interface {
8 | Query() TimelineQuery
9 | Records() []Message
10 | HasMore() bool
11 | RetrievedAt() time.Time
12 | }
13 |
--------------------------------------------------------------------------------
/service/ogp/parser/domain.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 | "time"
7 |
8 | "github.com/dyatlov/go-opengraph/opengraph"
9 | )
10 |
11 | var client = http.Client{
12 | Timeout: 5 * time.Second,
13 | }
14 |
15 | // X(Twitter)のOGPを取得するのにuserAgentの中にbotという文字列が入っている必要がある
16 | // Spotifyの新しいOGPを取得するのにuserAgentの中にcurl-botという文字列が入っている必要がある
17 | const userAgent = "traq-ogp-fetcher-curl-bot; contact: github.com/traPtitech/traQ"
18 |
19 | func FetchSpecialDomainInfo(url *url.URL) (og *opengraph.OpenGraph, meta *DefaultPageMeta, isSpecialDomain bool, err error) {
20 | switch url.Host {
21 | case "vrchat.com":
22 | og, meta, err = FetchVRChatInfo(url)
23 | return og, meta, true, err
24 | }
25 | return nil, nil, false, nil
26 | }
27 |
--------------------------------------------------------------------------------
/service/ogp/parser/domain_vrchat_test.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func Test_fetchVRChatWorldInfo(t *testing.T) {
12 | tests := []struct {
13 | name string
14 | worldID string
15 | want func(t *testing.T, res *VRChatAPIWorldResponse)
16 | wantErr assert.ErrorAssertionFunc
17 | }{
18 | {
19 | name: "success",
20 | worldID: "wrld_aa762efb-17b3-4302-8f41-09c4db2489ed",
21 | want: func(t *testing.T, res *VRChatAPIWorldResponse) {
22 | assert.Equal(t, "PROJECT˸ SUMMER FLARE", res.Name)
23 | assert.Equal(t, "Break the Summer․ Break the tower․ Complete the Meridian Loop․ A story about reality and humanity․", res.Description)
24 | assert.True(t, strings.HasPrefix(res.ImageURL, "https://"))
25 | assert.True(t, strings.HasPrefix(res.ThumbnailImageURL, "https://"))
26 | },
27 | wantErr: assert.NoError,
28 | },
29 | {
30 | name: "not found",
31 | worldID: "wrld_aa762efb-17b3-4302-8f41-09c4db2489ee",
32 | want: func(_ *testing.T, _ *VRChatAPIWorldResponse) {
33 | },
34 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
35 | return assert.ErrorIs(t, err, ErrClient, i...)
36 | },
37 | },
38 | }
39 | for _, tt := range tests {
40 | t.Run(tt.name, func(t *testing.T) {
41 | got, err := fetchVRChatWorldInfo(tt.worldID)
42 | if !tt.wantErr(t, err, fmt.Sprintf("fetchVRChatWorldInfo(%v)", tt.worldID)) {
43 | return
44 | }
45 | tt.want(t, got)
46 | })
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/service/ogp/parser/errors.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import "errors"
4 |
5 | var (
6 | // ErrParse 対象URLをHTMLとしてパースできませんでした
7 | ErrParse = errors.New("parse error")
8 | // ErrNetwork 対象URLにアクセスできませんでした
9 | ErrNetwork = errors.New("network error")
10 | // ErrContentTypeNotSupported 対象URLのコンテンツがサポートされていないContent-Typeを持っていました
11 | ErrContentTypeNotSupported = errors.New("content type not supported")
12 | // ErrClient 対象URLにアクセスした際に4xxエラーが発生しました
13 | ErrClient = errors.New("network error (client)")
14 | // ErrServer 対象URLにアクセスした際に5xxエラーが発生しました
15 | ErrServer = errors.New("network error (server)")
16 | // ErrDomainRequest 特殊処理を行うドメインのURLが期待した形式ではありませんでした
17 | ErrDomainRequest = errors.New("bad request for special domain ")
18 | )
19 |
--------------------------------------------------------------------------------
/service/ogp/service.go:
--------------------------------------------------------------------------------
1 | package ogp
2 |
3 | import (
4 | "net/url"
5 | "time"
6 |
7 | "github.com/traPtitech/traQ/model"
8 | )
9 |
10 | const DefaultCacheDuration = time.Hour * 24 * 7
11 |
12 | // Service OGPサービス
13 | type Service interface {
14 | // Shutdown OGPサービスを停止します
15 | Shutdown() error
16 |
17 | // GetMeta 指定したURLのメタタグをパースした結果を返します。
18 | //
19 | // 成功した場合、*model.Ogp、expiresAt、nil を返します。
20 | // URLに対応する情報が存在しない場合、nil、expiresAt、nilを返します。
21 | // 情報が存在する場合としない場合両方において expiresAt までキャッシュが可能です。
22 | //
23 | // 内部エラーが発生した場合、nil, 0, err を返します。
24 | GetMeta(url *url.URL) (ogp *model.Ogp, expiresAt time.Time, err error)
25 |
26 | // DeleteCache 指定したURLのキャッシュを削除します。
27 | DeleteCache(url *url.URL) error
28 | }
29 |
--------------------------------------------------------------------------------
/service/qall/soundboard.go:
--------------------------------------------------------------------------------
1 | package qall
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/gofrs/uuid"
7 | "github.com/traPtitech/traQ/model"
8 | )
9 |
10 | type Soundboard interface {
11 | // SaveSoundboardItem サウンドボードアイテムを保存します
12 | // 成功した場合、nilを返します
13 | // イベント通知機能が実装されている場合、アイテム作成時にQallSoundboardItemCreatedイベントが発行されます
14 | SaveSoundboardItem(soundID uuid.UUID, soundName string, contentType string, fileType model.FileType, src io.Reader, stampID *uuid.UUID, creatorID uuid.UUID) error
15 | // GetURL Pre-signed URLを取得します
16 | // 有効期限は5分です
17 | // 成功した場合、URLとnilを返します
18 | GetURL(soundID uuid.UUID) (string, error)
19 | // DeleteSoundboardItem サウンドボードアイテムを削除します
20 | // 成功した場合、nilを返します
21 | // イベント通知機能が実装されている場合、アイテム削除時にQallSoundboardItemDeletedイベントが発行されます
22 | DeleteSoundboardItem(soundID uuid.UUID) error
23 | }
24 |
--------------------------------------------------------------------------------
/service/rbac/permission/bot.go:
--------------------------------------------------------------------------------
1 | package permission
2 |
3 | const (
4 | // GetWebhook Webhook情報取得権限
5 | GetWebhook = Permission("get_webhook")
6 | // CreateWebhook Webhook作成権限
7 | CreateWebhook = Permission("create_webhook")
8 | // EditWebhook Webhook編集権限
9 | EditWebhook = Permission("edit_webhook")
10 | // DeleteWebhook Webhook削除権限
11 | DeleteWebhook = Permission("delete_webhook")
12 | // AccessOthersWebhook 他人のWebhookのアクセス権限
13 | AccessOthersWebhook = Permission("access_others_webhook")
14 |
15 | // GetBot Bot情報取得権限
16 | GetBot = Permission("get_bot")
17 | // CreateBot Bot作成権限
18 | CreateBot = Permission("create_bot")
19 | // EditBot Bot編集権限
20 | EditBot = Permission("edit_bot")
21 | // DeleteBot Bot削除権限
22 | DeleteBot = Permission("delete_bot")
23 | // AccessOthersBot 他人のBotのアクセス権限
24 | AccessOthersBot = Permission("access_others_bot")
25 |
26 | // BotActionJoinChannel BOTアクション実行権限:チャンネル参加
27 | BotActionJoinChannel = Permission("bot_action_join_channel")
28 | // BotActionLeaveChannel BOTアクション実行権限:チャンネル退出
29 | BotActionLeaveChannel = Permission("bot_action_leave_channel")
30 | )
31 |
--------------------------------------------------------------------------------
/service/rbac/permission/channels.go:
--------------------------------------------------------------------------------
1 | package permission
2 |
3 | const (
4 | // CreateChannel チャンネル作成権限
5 | CreateChannel = Permission("create_channel")
6 | // GetChannel チャンネル情報取得権限
7 | GetChannel = Permission("get_channel")
8 | // EditChannel チャンネル情報変更権限
9 | EditChannel = Permission("edit_channel")
10 | // DeleteChannel チャンネル削除権限
11 | DeleteChannel = Permission("delete_channel")
12 | // ChangeParentChannel 親チャンネル変更権限
13 | ChangeParentChannel = Permission("change_parent_channel")
14 | // EditChannelTopic チャンネルトピック変更権限
15 | EditChannelTopic = Permission("edit_channel_topic")
16 | // GetChannelStar チャンネルスター取得権限
17 | GetChannelStar = Permission("get_channel_star")
18 | // EditChannelStar チャンネルスター編集権限
19 | EditChannelStar = Permission("edit_channel_star")
20 | )
21 |
--------------------------------------------------------------------------------
/service/rbac/permission/client.go:
--------------------------------------------------------------------------------
1 | package permission
2 |
3 | const (
4 | // GetMyTokens 自トークン情報取得権限
5 | GetMyTokens = Permission("get_my_tokens")
6 | // RevokeMyToken 自トークン削除権限
7 | RevokeMyToken = Permission("revoke_my_token")
8 | // GetClients クライアント情報取得権限
9 | GetClients = Permission("get_clients")
10 | // CreateClient 新規クライアント登録権限
11 | CreateClient = Permission("create_client")
12 | // EditMyClient クライアント情報編集権限
13 | EditMyClient = Permission("edit_my_client")
14 | // DeleteMyClient クライアント削除権限
15 | DeleteMyClient = Permission("delete_my_client")
16 | // ManageOthersClient 他人のClientの管理権限
17 | ManageOthersClient = Permission("manage_others_client")
18 | )
19 |
--------------------------------------------------------------------------------
/service/rbac/permission/file.go:
--------------------------------------------------------------------------------
1 | package permission
2 |
3 | const (
4 | // UploadFile ファイルアップロード権限
5 | UploadFile = Permission("upload_file")
6 | // DownloadFile ファイルダウンロード権限
7 | DownloadFile = Permission("download_file")
8 | // DeleteFile ファイル削除権限
9 | DeleteFile = Permission("delete_file")
10 | )
11 |
--------------------------------------------------------------------------------
/service/rbac/permission/message.go:
--------------------------------------------------------------------------------
1 | package permission
2 |
3 | const (
4 | // GetMessage メッセージ取得権限
5 | GetMessage = Permission("get_message")
6 | // PostMessage メッセージ投稿権限
7 | PostMessage = Permission("post_message")
8 | // EditMessage メッセージ編集権限
9 | EditMessage = Permission("edit_message")
10 | // DeleteMessage メッセージ削除権限
11 | DeleteMessage = Permission("delete_message")
12 | // ReportMessage メッセージ通報権限
13 | ReportMessage = Permission("report_message")
14 | // GetMessageReports メッセージ通報取得権限
15 | GetMessageReports = Permission("get_message_reports")
16 | // CreateMessagePin ピン留め作成権限
17 | CreateMessagePin = Permission("create_message_pin")
18 | // DeleteMessagePin ピン留め削除権限
19 | DeleteMessagePin = Permission("delete_message_pin")
20 | )
21 |
--------------------------------------------------------------------------------
/service/rbac/permission/notification.go:
--------------------------------------------------------------------------------
1 | package permission
2 |
3 | const (
4 | // GetChannelSubscription チャンネル購読状況取得権限
5 | GetChannelSubscription = Permission("get_channel_subscription")
6 | // EditChannelSubscription チャンネル購読変更権限
7 | EditChannelSubscription = Permission("edit_channel_subscription")
8 | // ConnectNotificationStream 通知ストリームへの接続権限
9 | ConnectNotificationStream = Permission("connect_notification_stream")
10 | // RegisterFCMDevice FCMデバイスの登録権限
11 | RegisterFCMDevice = Permission("register_fcm_device")
12 | )
13 |
--------------------------------------------------------------------------------
/service/rbac/permission/stamp.go:
--------------------------------------------------------------------------------
1 | package permission
2 |
3 | const (
4 | // GetStamp スタンプ情報取得権限
5 | GetStamp = Permission("get_stamp")
6 | // CreateStamp スタンプ作成権限
7 | CreateStamp = Permission("create_stamp")
8 | // EditStamp 自スタンプ画像変更権限
9 | EditStamp = Permission("edit_stamp")
10 | // EditStampCreatedByOthers 他ユーザー作成のスタンプの変更権限
11 | EditStampCreatedByOthers = Permission("edit_stamp_created_by_others")
12 | // DeleteStamp スタンプ削除権限
13 | DeleteStamp = Permission("delete_stamp")
14 | // DeleteMyStamp 自分のスタンプ削除権限
15 | DeleteMyStamp = Permission("delete_my_stamp")
16 | // AddMessageStamp メッセージスタンプ追加権限
17 | AddMessageStamp = Permission("add_message_stamp")
18 | // RemoveMessageStamp メッセージスタンプ削除権限
19 | RemoveMessageStamp = Permission("remove_message_stamp")
20 | // GetMyStampHistory 自分のスタンプ履歴取得権限
21 | GetMyStampHistory = Permission("get_my_stamp_history")
22 |
23 | // GetStampPalette スタンプパレット取得権限
24 | GetStampPalette = Permission("get_stamp_palette")
25 | // CreateStampPalette スタンプパレット作成権限
26 | CreateStampPalette = Permission("create_stamp_palette")
27 | // EditStampPalette スタンプパレット編集権限
28 | EditStampPalette = Permission("edit_stamp_palette")
29 | // DeleteStampPalette スタンプパレット削除権限
30 | DeleteStampPalette = Permission("delete_stamp_palette")
31 | )
32 |
--------------------------------------------------------------------------------
/service/rbac/rbac.go:
--------------------------------------------------------------------------------
1 | package rbac
2 |
3 | import "github.com/traPtitech/traQ/service/rbac/permission"
4 |
5 | // RBAC Role-based Access Controllerインターフェース
6 | type RBAC interface {
7 | // Reload 全権限を読み込み直します
8 | Reload() error
9 |
10 | // IsGranted 指定したロールで指定した権限が許可されているかどうか
11 | IsGranted(role string, perm permission.Permission) bool
12 | // IsAllGranted 指定したロール全てで指定した権限が許可されているかどうか
13 | IsAllGranted(roles []string, perm permission.Permission) bool
14 | // IsAnyGranted 指定したロールのいずれかで指定した権限が許可されているかどうか
15 | IsAnyGranted(roles []string, perm permission.Permission) bool
16 | // GetGrantedPermissions 指定したロールに与えられている全ての権限を取得します
17 | GetGrantedPermissions(role string) []permission.Permission
18 | }
19 |
--------------------------------------------------------------------------------
/service/rbac/role/admin.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | // Admin 管理者ユーザーロール
4 | const Admin = "admin"
5 |
--------------------------------------------------------------------------------
/service/rbac/role/bot.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac/permission"
5 | )
6 |
7 | // Bot Botユーザーロール
8 | const Bot = "bot"
9 |
10 | var botPerms = []permission.Permission{
11 | permission.GetChannel,
12 | permission.EditChannelTopic,
13 | permission.GetMessage,
14 | permission.PostMessage,
15 | permission.EditMessage,
16 | permission.DeleteMessage,
17 | permission.CreateMessagePin,
18 | permission.DeleteMessagePin,
19 | permission.GetChannelSubscription,
20 | permission.EditChannelSubscription,
21 | permission.GetUser,
22 | permission.GetMe,
23 | permission.GetOIDCUserInfo,
24 | permission.EditMe,
25 | permission.GetMyStampHistory,
26 | permission.ChangeMyIcon,
27 | permission.GetUserTag,
28 | permission.EditUserTag,
29 | permission.GetUserGroup,
30 | permission.CreateUserGroup,
31 | permission.EditUserGroup,
32 | permission.DeleteUserGroup,
33 | permission.GetStamp,
34 | permission.AddMessageStamp,
35 | permission.RemoveMessageStamp,
36 | permission.DownloadFile,
37 | permission.UploadFile,
38 | permission.DeleteFile,
39 | permission.BotActionJoinChannel,
40 | permission.BotActionLeaveChannel,
41 | permission.WebRTC,
42 | }
43 |
--------------------------------------------------------------------------------
/service/rbac/role/client.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac/permission"
5 | )
6 |
7 | // Client Clientロール (for OAuth2 client credentials grant)
8 | const Client = "client"
9 |
10 | // 自分自身以外の参照系は許可するようにしたいが、https://github.com/traPtitech/traQ/pull/2433#discussion_r1649383346
11 | // の事情から許可できる権限が限られる
12 | // https://github.com/traPtitech/traQ/issues/2463 で権限を増やせるよう対応予定
13 | var clientPerms = []permission.Permission{
14 | permission.GetUser,
15 | permission.GetUserTag,
16 | permission.GetUserGroup,
17 | permission.GetStamp,
18 | }
19 |
--------------------------------------------------------------------------------
/service/rbac/role/manage_bot.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac/permission"
5 | )
6 |
7 | // ManageBot BotConsoleロール
8 | const ManageBot = "manage_bot"
9 |
10 | var manageBotPerms = []permission.Permission{
11 | permission.GetChannel,
12 | permission.GetUser,
13 | permission.GetMe,
14 | permission.GetOIDCUserInfo,
15 | permission.GetWebhook,
16 | permission.CreateWebhook,
17 | permission.EditWebhook,
18 | permission.DeleteWebhook,
19 | permission.GetBot,
20 | permission.CreateBot,
21 | permission.EditBot,
22 | permission.DeleteBot,
23 | permission.BotActionJoinChannel,
24 | permission.BotActionLeaveChannel,
25 | permission.GetClients,
26 | permission.CreateClient,
27 | permission.EditMyClient,
28 | permission.DeleteMyClient,
29 | }
30 |
--------------------------------------------------------------------------------
/service/rbac/role/openid.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac/permission"
5 | )
6 |
7 | // OpenID OIDC専用ロール
8 | const OpenID = "openid"
9 |
10 | var openIDPerms []permission.Permission
11 |
--------------------------------------------------------------------------------
/service/rbac/role/profile.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac/permission"
5 | )
6 |
7 | // Profile ユーザー情報読み取り専用ロール (for OIDC)
8 | const Profile = "profile"
9 |
10 | var profilePerms = []permission.Permission{
11 | permission.GetOIDCUserInfo,
12 | }
13 |
--------------------------------------------------------------------------------
/service/rbac/role/read.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac/permission"
5 | )
6 |
7 | // Read 読み取り専用ユーザーロール
8 | const Read = "read"
9 |
10 | var readPerms = []permission.Permission{
11 | permission.GetChannel,
12 | permission.GetMessage,
13 | permission.GetChannelSubscription,
14 | permission.ConnectNotificationStream,
15 | permission.GetUser,
16 | permission.GetMe,
17 | permission.GetOIDCUserInfo,
18 | permission.GetChannelStar,
19 | permission.GetUnread,
20 | permission.GetUserTag,
21 | permission.GetUserGroup,
22 | permission.GetStamp,
23 | permission.GetMyStampHistory,
24 | permission.DownloadFile,
25 | permission.GetWebhook,
26 | permission.GetBot,
27 | permission.GetClipFolder,
28 | permission.GetStampPalette,
29 | }
30 |
--------------------------------------------------------------------------------
/service/rbac/role/user.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac/permission"
5 | )
6 |
7 | // User 一般ユーザーロール
8 | const User = "user"
9 |
10 | var userPerms = []permission.Permission{
11 | // read, writeロールのパーミッションを全て含む
12 | permission.ChangeMyPassword,
13 | permission.GetUserQRCode,
14 | permission.GetMySessions,
15 | permission.DeleteMySessions,
16 | permission.GetMyTokens,
17 | permission.RevokeMyToken,
18 | permission.GetMyExternalAccount,
19 | permission.EditMyExternalAccount,
20 | permission.GetClients,
21 | permission.CreateClient,
22 | permission.EditMyClient,
23 | permission.DeleteMyClient,
24 | permission.CreateWebhook,
25 | permission.EditWebhook,
26 | permission.DeleteWebhook,
27 | permission.CreateBot,
28 | permission.EditBot,
29 | permission.DeleteBot,
30 | permission.BotActionJoinChannel,
31 | permission.BotActionLeaveChannel,
32 | permission.WebRTC,
33 | }
34 |
35 | func init() {
36 | userPerms = append(userPerms, readPerms...)
37 | userPerms = append(userPerms, writePerms...)
38 | }
39 |
--------------------------------------------------------------------------------
/service/rbac/role/write.go:
--------------------------------------------------------------------------------
1 | package role
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac/permission"
5 | )
6 |
7 | // Write 書き込み専用ユーザーロール
8 | const Write = "write"
9 |
10 | var writePerms = []permission.Permission{
11 | permission.CreateChannel,
12 | permission.EditChannelTopic,
13 | permission.PostMessage,
14 | permission.EditMessage,
15 | permission.DeleteMessage,
16 | permission.ReportMessage,
17 | permission.CreateMessagePin,
18 | permission.DeleteMessagePin,
19 | permission.EditChannelSubscription,
20 | permission.RegisterFCMDevice,
21 | permission.EditMe,
22 | permission.ChangeMyIcon,
23 | permission.EditChannelStar,
24 | permission.DeleteUnread,
25 | permission.EditUserTag,
26 | permission.CreateUserGroup,
27 | permission.EditUserGroup,
28 | permission.DeleteUserGroup,
29 | permission.CreateStamp,
30 | permission.DeleteMyStamp,
31 | permission.AddMessageStamp,
32 | permission.RemoveMessageStamp,
33 | permission.EditStamp,
34 | permission.UploadFile,
35 | permission.DeleteFile,
36 | permission.CreateClipFolder,
37 | permission.EditClipFolder,
38 | permission.DeleteClipFolder,
39 | permission.CreateStampPalette,
40 | permission.EditStampPalette,
41 | permission.DeleteStampPalette,
42 | }
43 |
--------------------------------------------------------------------------------
/service/search/null.go:
--------------------------------------------------------------------------------
1 | package search
2 |
3 | var nullE = &nullEngine{}
4 |
5 | type nullEngine struct{}
6 |
7 | // NewNullEngine 常に利用不可な検索エンジンを返します
8 | func NewNullEngine() Engine {
9 | return nullE
10 | }
11 |
12 | func (n *nullEngine) Do(*Query) (Result, error) {
13 | return nil, ErrServiceUnavailable
14 | }
15 |
16 | func (n *nullEngine) Available() bool {
17 | return false
18 | }
19 |
20 | func (n *nullEngine) Close() error {
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/service/services_wire.go:
--------------------------------------------------------------------------------
1 | //go:build wireinject
2 | // +build wireinject
3 |
4 | package service
5 |
6 | import (
7 | "github.com/google/wire"
8 | )
9 |
10 | var ProviderSet = wire.NewSet(wire.FieldsOf(new(*Services),
11 | "BOT",
12 | "ChannelManager",
13 | "OnlineCounter",
14 | "UnreadMessageCounter",
15 | "MessageCounter",
16 | "UserCounter"
17 | "ChannelCounter",
18 | "StampThrottler",
19 | "FCM",
20 | "FileManager",
21 | "Imaging",
22 | "MessageManager",
23 | "Notification",
24 | "OGP",
25 | "OIDC",
26 | "RBAC",
27 | "Search",
28 | "ViewerManager",
29 | "WebRTCv3",
30 | "WS",
31 | "BotWS",
32 | "QallRoomStateManager",
33 | "QallSoundBoard",
34 | ))
35 |
--------------------------------------------------------------------------------
/service/variable/variable_type.go:
--------------------------------------------------------------------------------
1 | package variable
2 |
3 | type ServerOriginString string
4 |
5 | type FirebaseCredentialsFilePathString string
6 |
--------------------------------------------------------------------------------
/service/viewer/state.go:
--------------------------------------------------------------------------------
1 | package viewer
2 |
3 | import (
4 | "strings"
5 |
6 | jsonIter "github.com/json-iterator/go"
7 | )
8 |
9 | // State 閲覧状態
10 | type State int
11 |
12 | const (
13 | // StateNone バックグランド表示中
14 | StateNone State = iota
15 | // StateMonitoring メッセージ表示中
16 | StateMonitoring
17 | // StateEditing メッセージ入力中
18 | StateEditing
19 | )
20 |
21 | // String string表記にします
22 | func (s State) String() string {
23 | return viewStateStrings[s]
24 | }
25 |
26 | // MarshalJSON encoding/json.Marshaler 実装
27 | func (s State) MarshalJSON() ([]byte, error) {
28 | return jsonIter.ConfigFastest.Marshal(s.String())
29 | }
30 |
31 | // StateFromString stringからviewer.Stateに変換します
32 | func StateFromString(s string) State {
33 | return stringViewStates[strings.ToLower(s)]
34 | }
35 |
36 | var (
37 | viewStateStrings = map[State]string{
38 | StateNone: "none",
39 | StateEditing: "editing",
40 | StateMonitoring: "monitoring",
41 | }
42 | stringViewStates = map[string]State{}
43 | )
44 |
45 | func init() {
46 | // 転置マップ生成
47 | for v, k := range viewStateStrings {
48 | stringViewStates[k] = v
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/service/viewer/state_with_channel.go:
--------------------------------------------------------------------------------
1 | package viewer
2 |
3 | import "github.com/gofrs/uuid"
4 |
5 | // StateWithChannel 閲覧状態
6 | type StateWithChannel struct {
7 | State State
8 | ChannelID uuid.UUID
9 | }
10 |
--------------------------------------------------------------------------------
/service/viewer/state_with_time.go:
--------------------------------------------------------------------------------
1 | package viewer
2 |
3 | import "time"
4 |
5 | // StateWithTime 閲覧状態
6 | type StateWithTime struct {
7 | State State
8 | Time time.Time
9 | }
10 |
--------------------------------------------------------------------------------
/service/viewer/user_state.go:
--------------------------------------------------------------------------------
1 | package viewer
2 |
3 | import (
4 | "sort"
5 | "time"
6 |
7 | "github.com/gofrs/uuid"
8 | )
9 |
10 | // UserState ユーザー閲覧状態
11 | type UserState struct {
12 | UserID uuid.UUID `json:"userId"`
13 | State State `json:"state"`
14 | UpdatedAt time.Time `json:"updatedAt"`
15 | }
16 |
17 | // UserStates []UserState
18 | type UserStates []UserState
19 |
20 | // Len implements sort.Interface
21 | func (u UserStates) Len() int {
22 | return len(u)
23 | }
24 |
25 | // Less implements sort.Interface
26 | func (u UserStates) Less(i, j int) bool {
27 | return u[i].UpdatedAt.Before(u[j].UpdatedAt)
28 | }
29 |
30 | // Swap implements sort.Interface
31 | func (u UserStates) Swap(i, j int) {
32 | u[i], u[j] = u[j], u[i]
33 | }
34 |
35 | // ConvertToArray 閲覧状態mapをsliceに変換します
36 | func ConvertToArray(cv map[uuid.UUID]StateWithTime) UserStates {
37 | result := make(UserStates, 0, len(cv))
38 | for uid, swt := range cv {
39 | result = append(result, UserState{
40 | UserID: uid,
41 | State: swt.State,
42 | UpdatedAt: swt.Time,
43 | })
44 | }
45 | sort.Sort(result)
46 | return result
47 | }
48 |
--------------------------------------------------------------------------------
/service/ws/config.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/gorilla/websocket"
8 | jsonIter "github.com/json-iterator/go"
9 | )
10 |
11 | const (
12 | writeWait = 5 * time.Second
13 | pongWait = 60 * time.Second
14 | pingPeriod = (pongWait * 9) / 10
15 | maxReadMessageSize = 1 << 9 // 512B
16 | messageBufferSize = 256
17 | )
18 |
19 | var (
20 | json = jsonIter.ConfigFastest
21 | upgrader = &websocket.Upgrader{
22 | ReadBufferSize: 1024,
23 | WriteBufferSize: 1024,
24 | CheckOrigin: func(_ *http.Request) bool { return true },
25 | }
26 | )
27 |
--------------------------------------------------------------------------------
/service/ws/message.go:
--------------------------------------------------------------------------------
1 | package ws
2 |
3 | type rawMessage struct {
4 | t int
5 | data []byte
6 | }
7 |
8 | type message struct {
9 | Type string `json:"type"`
10 | Body interface{} `json:"body"`
11 | }
12 |
13 | func makeMessage(t string, b interface{}) (m *message) {
14 | return &message{
15 | Type: t,
16 | Body: b,
17 | }
18 | }
19 |
20 | func (m *message) toJSON() (b []byte) {
21 | b, _ = json.Marshal(m)
22 | return
23 | }
24 |
--------------------------------------------------------------------------------
/testdata/gif/embed.go:
--------------------------------------------------------------------------------
1 | package gif
2 |
3 | import "embed"
4 |
5 | // FS migration file system
6 | //
7 | //go:embed *.gif
8 | var FS embed.FS
9 |
10 | // GIF画像 出典
11 |
12 | // frog.gif https://sozai-good.com/illust/gifanimation/29065
13 |
14 | // miku.gif https://piapro.jp/t/FB3J
15 | // marucaさんの作品
16 |
17 | // mushroom.gif http://www.ugokue.com
18 | // 【動け!!動く絵】様より
19 |
20 | // new_year.gif https://freesozaixtrain.web.fc2.com/freesozai-nenga-train2.html
21 |
22 | // tooth.gif https://patirabi.com/2021/10/10/061gif/
23 |
24 | // surprised.gif https://patirabi.com/2021/10/08/058gif/
25 |
--------------------------------------------------------------------------------
/testdata/gif/frog.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/frog.gif
--------------------------------------------------------------------------------
/testdata/gif/frog_resized.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/frog_resized.gif
--------------------------------------------------------------------------------
/testdata/gif/miku.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/miku.gif
--------------------------------------------------------------------------------
/testdata/gif/miku_resized.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/miku_resized.gif
--------------------------------------------------------------------------------
/testdata/gif/mushroom.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/mushroom.gif
--------------------------------------------------------------------------------
/testdata/gif/mushroom_resized.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/mushroom_resized.gif
--------------------------------------------------------------------------------
/testdata/gif/new_year.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/new_year.gif
--------------------------------------------------------------------------------
/testdata/gif/new_year_resized.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/new_year_resized.gif
--------------------------------------------------------------------------------
/testdata/gif/surprised.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/surprised.gif
--------------------------------------------------------------------------------
/testdata/gif/surprised_resized.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/surprised_resized.gif
--------------------------------------------------------------------------------
/testdata/gif/tooth.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/tooth.gif
--------------------------------------------------------------------------------
/testdata/gif/tooth_resized.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/gif/tooth_resized.gif
--------------------------------------------------------------------------------
/testdata/images/embed.go:
--------------------------------------------------------------------------------
1 | package images
2 |
3 | import "embed"
4 |
5 | // FS migration file system
6 | //
7 | //go:embed *.png
8 | var ImageFS embed.FS
9 |
--------------------------------------------------------------------------------
/testdata/images/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/images/test.png
--------------------------------------------------------------------------------
/testdata/images/test_fit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/images/test_fit.png
--------------------------------------------------------------------------------
/testdata/images/test_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/traPtitech/traQ/4404a2717858a6fc663520fb4eb5efdc71d12786/testdata/images/test_thumbnail.png
--------------------------------------------------------------------------------
/testutils/empty_test_repository.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "github.com/traPtitech/traQ/repository"
5 | )
6 |
7 | type EmptyTestRepository struct {
8 | repository.UserRepository
9 | repository.UserGroupRepository
10 | repository.UserSettingsRepository
11 | repository.UserRoleRepository
12 | repository.TagRepository
13 | repository.ChannelRepository
14 | repository.MessageRepository
15 | repository.MessageReportRepository
16 | repository.StampRepository
17 | repository.StampPaletteRepository
18 | repository.StarRepository
19 | repository.PinRepository
20 | repository.DeviceRepository
21 | repository.FileRepository
22 | repository.WebhookRepository
23 | repository.OAuth2Repository
24 | repository.BotRepository
25 | repository.ClipRepository
26 | repository.OgpCacheRepository
27 | repository.SoundboardRepository
28 | }
29 |
--------------------------------------------------------------------------------
/testutils/gif.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "io/fs"
5 |
6 | "github.com/traPtitech/traQ/testdata/gif"
7 | )
8 |
9 | func MustOpenGif(name string) fs.File {
10 | f, err := gif.FS.Open(name)
11 | if err != nil {
12 | panic(err)
13 | }
14 | return f
15 | }
16 |
--------------------------------------------------------------------------------
/testutils/test_rbac.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "github.com/traPtitech/traQ/service/rbac"
5 | "github.com/traPtitech/traQ/service/rbac/permission"
6 | "github.com/traPtitech/traQ/service/rbac/role"
7 | )
8 |
9 | type rbacImpl struct {
10 | roles role.Roles
11 | }
12 |
13 | func NewTestRBAC() rbac.RBAC {
14 | return &rbacImpl{
15 | roles: role.GetSystemRoles(),
16 | }
17 | }
18 |
19 | func (rbac *rbacImpl) Reload() error {
20 | return nil
21 | }
22 |
23 | func (rbac *rbacImpl) IsGranted(r string, p permission.Permission) bool {
24 | if r == role.Admin {
25 | return true
26 | }
27 | return rbac.roles.HasAndIsGranted(r, p)
28 | }
29 |
30 | func (rbac *rbacImpl) IsAllGranted(roles []string, perm permission.Permission) bool {
31 | for _, role := range roles {
32 | if !rbac.IsGranted(role, perm) {
33 | return false
34 | }
35 | }
36 | return true
37 | }
38 |
39 | func (rbac *rbacImpl) IsAnyGranted(roles []string, perm permission.Permission) bool {
40 | for _, role := range roles {
41 | if rbac.IsGranted(role, perm) {
42 | return true
43 | }
44 | }
45 | return false
46 | }
47 |
48 | func (rbac *rbacImpl) GetGrantedPermissions(roleName string) []permission.Permission {
49 | ro, ok := rbac.roles[roleName]
50 | if ok {
51 | return ro.Permissions().Array()
52 | }
53 | return nil
54 | }
55 |
--------------------------------------------------------------------------------
/tools.go:
--------------------------------------------------------------------------------
1 | //go:build tools
2 | // +build tools
3 |
4 | package main
5 |
6 | import (
7 | _ "github.com/golang/mock/mockgen"
8 | _ "github.com/google/wire/cmd/wire"
9 | )
10 |
--------------------------------------------------------------------------------
/utils/gormutil/error.go:
--------------------------------------------------------------------------------
1 | package gormutil
2 |
3 | import (
4 | "github.com/go-sql-driver/mysql"
5 | )
6 |
7 | const (
8 | errMySQLDuplicatedRecord uint16 = 1062
9 | errMySQLForeignKeyConstraintFails uint16 = 1452
10 | )
11 |
12 | // IsMySQLDuplicatedRecordErr MySQL重複レコードエラーかどうか
13 | func IsMySQLDuplicatedRecordErr(err error) bool {
14 | mErr, ok := err.(*mysql.MySQLError)
15 | if !ok {
16 | return false
17 | }
18 | return mErr.Number == errMySQLDuplicatedRecord
19 | }
20 |
21 | // IsMySQLForeignKeyConstraintFailsError MySQL外部キー制約エラーかどうか
22 | func IsMySQLForeignKeyConstraintFailsError(err error) bool {
23 | mErr, ok := err.(*mysql.MySQLError)
24 | if !ok {
25 | return false
26 | }
27 | return mErr.Number == errMySQLForeignKeyConstraintFails
28 | }
29 |
--------------------------------------------------------------------------------
/utils/gormutil/util.go:
--------------------------------------------------------------------------------
1 | package gormutil
2 |
3 | import "gorm.io/gorm"
4 |
5 | // RecordExists 指定した条件のレコードが1行以上存在するかどうか
6 | func RecordExists(db *gorm.DB, where interface{}, tableName ...string) (exists bool, err error) {
7 | if len(tableName) > 0 {
8 | db = db.Table(tableName[0])
9 | } else {
10 | db = db.Model(where)
11 | }
12 | return Exists(db.Where(where))
13 | }
14 |
15 | // Exists 行数が1行以上かどうかを返します
16 | func Exists(db *gorm.DB) (exists bool, err error) {
17 | n, err := Count(db.Limit(1))
18 | return n > 0, err
19 | }
20 |
21 | // Count 行数を数えます
22 | func Count(db *gorm.DB) (n int64, err error) {
23 | return n, db.Count(&n).Error
24 | }
25 |
26 | // LimitAndOffset limit句とoffset句を指定します。値が0以下の場合は指定されません。
27 | func LimitAndOffset(limit, offset int) func(db *gorm.DB) *gorm.DB {
28 | return func(db *gorm.DB) *gorm.DB {
29 | if offset > 0 {
30 | db = db.Offset(offset)
31 | }
32 | if limit > 0 {
33 | db = db.Limit(limit)
34 | }
35 | return db
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/utils/hmac/hmac.go:
--------------------------------------------------------------------------------
1 | package hmac
2 |
3 | import (
4 | "crypto/hmac"
5 | "crypto/sha1"
6 | "crypto/sha256"
7 | )
8 |
9 | // SHA1 HMAC-SHA-1を計算します
10 | func SHA1(data []byte, secret string) []byte {
11 | mac := hmac.New(sha1.New, []byte(secret))
12 | _, _ = mac.Write(data)
13 | return mac.Sum(nil)
14 | }
15 |
16 | // SHA256 HMAC-SHA-256を計算します
17 | func SHA256(data []byte, secret string) []byte {
18 | mac := hmac.New(sha256.New, []byte(secret))
19 | _, _ = mac.Write(data)
20 | return mac.Sum(nil)
21 | }
22 |
--------------------------------------------------------------------------------
/utils/imaging/gif.go:
--------------------------------------------------------------------------------
1 | package imaging
2 |
3 | import (
4 | "bytes"
5 | "image/gif"
6 | )
7 |
8 | // GifToBytesReader GIF画像を*bytes.Readerに書き出します
9 | func GifToBytesReader(src *gif.GIF) (*bytes.Reader, error) {
10 | buf := new(bytes.Buffer)
11 | if err := gif.EncodeAll(buf, src); err != nil {
12 | return nil, err
13 | }
14 | return bytes.NewReader(buf.Bytes()), nil
15 | }
16 |
--------------------------------------------------------------------------------
/utils/imaging/gif_test.go:
--------------------------------------------------------------------------------
1 | package imaging
2 |
3 | import (
4 | "image/gif"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/traPtitech/traQ/testutils"
9 | )
10 |
11 | func TestGifToBytesReader(t *testing.T) {
12 | t.Parallel()
13 |
14 | tests := []string{"miku.gif", "new_year.gif", "tooth.gif"}
15 |
16 | for _, tt := range tests {
17 | tt := tt
18 |
19 | t.Run(tt, func(t *testing.T) {
20 | t.Parallel()
21 |
22 | f, err := gif.DecodeAll(testutils.MustOpenGif(tt))
23 | assert.Nil(t, err)
24 |
25 | _, err = GifToBytesReader(f)
26 | assert.Nil(t, err)
27 | })
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/utils/imaging/icon.go:
--------------------------------------------------------------------------------
1 | package imaging
2 |
3 | import (
4 | "image"
5 |
6 | "github.com/motoki317/go-identicon"
7 | )
8 |
9 | const iconSize = 256
10 |
11 | var iconSettings = identicon.DefaultSettings()
12 |
13 | // GenerateIcon アイコン画像を生成します
14 | func GenerateIcon(salt string) (image.Image, error) {
15 | return identicon.Render(identicon.Code(salt), iconSize, iconSettings)
16 | }
17 |
--------------------------------------------------------------------------------
/utils/imaging/icon_test.go:
--------------------------------------------------------------------------------
1 | package imaging
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestGenerateIcon(t *testing.T) {
11 | t.Parallel()
12 |
13 | assert := assert.New(t)
14 |
15 | icon1, err := GenerateIcon("a")
16 | require.NoError(t, err)
17 | icon2, err := GenerateIcon("b")
18 | require.NoError(t, err)
19 | icon3, err := GenerateIcon("b")
20 | require.NoError(t, err)
21 |
22 | if assert.NotNil(icon1) && assert.NotNil(icon2) && assert.NotNil(icon3) {
23 | assert.NotEqual(icon1, icon2)
24 | assert.EqualValues(icon2, icon3)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/utils/keymutex.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "sync"
4 |
5 | // KeyMutex キーによるMutex
6 | type KeyMutex struct {
7 | locks []sync.Mutex
8 | count uint
9 | }
10 |
11 | // NewKeyMutex KeyMutexを生成します
12 | func NewKeyMutex(count uint) *KeyMutex {
13 | return &KeyMutex{
14 | count: count,
15 | locks: make([]sync.Mutex, count),
16 | }
17 | }
18 |
19 | // Lock キーをロックします
20 | func (m *KeyMutex) Lock(key string) {
21 | m.locks[elfHash(key)%m.count].Lock()
22 | }
23 |
24 | // Unlock キーをアンロックします
25 | func (m *KeyMutex) Unlock(key string) {
26 | m.locks[elfHash(key)%m.count].Unlock()
27 | }
28 |
29 | func elfHash(key string) uint {
30 | h := uint(0)
31 | for i := range len(key) {
32 | h = (h << 4) + uint(key[i])
33 | g := h & 0xF0000000
34 | if g != 0 {
35 | h ^= g >> 24
36 | }
37 | h &= ^g
38 | }
39 | return h
40 | }
41 |
--------------------------------------------------------------------------------
/utils/keymutex_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "sync"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestNewKeyMutex(t *testing.T) {
11 | t.Parallel()
12 | assert := assert.New(t)
13 |
14 | km := NewKeyMutex(10)
15 | if assert.NotNil(km) {
16 | assert.EqualValues(10, km.count)
17 | assert.Len(km.locks, 10)
18 | }
19 | }
20 |
21 | func TestKeyMutex_LockAndUnlock(t *testing.T) {
22 | t.Parallel()
23 | assert := assert.New(t)
24 |
25 | km := NewKeyMutex(10)
26 |
27 | counter := [10]int{}
28 | keys := []string{
29 | "test",
30 | "aiueo",
31 | "abcd",
32 | "12345",
33 | "foo",
34 | "bar",
35 | "a",
36 | "b",
37 | "1111",
38 | "eeee",
39 | }
40 |
41 | wg := sync.WaitGroup{}
42 | for i := range 100000 {
43 | wg.Add(1)
44 | go func(i int) {
45 | j := i % 10
46 | km.Lock(keys[j])
47 | counter[j]++
48 | km.Unlock(keys[j])
49 | wg.Done()
50 | }(i)
51 | }
52 | wg.Wait()
53 |
54 | for i := range 10 {
55 | assert.Equal(10000, counter[i])
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/utils/message/embedded.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "strings"
5 |
6 | jsonIter "github.com/json-iterator/go"
7 | )
8 |
9 | // EmbeddedInfo メッセージの埋め込み情報
10 | type EmbeddedInfo struct {
11 | Raw string `json:"raw"`
12 | Type string `json:"type"`
13 | ID string `json:"id"`
14 | }
15 |
16 | // ExtractEmbedding メッセージの埋め込み情報を抽出したものと、平文化したメッセージを返します
17 | func ExtractEmbedding(m string) (res []*EmbeddedInfo, plain string) {
18 | res = make([]*EmbeddedInfo, 0)
19 | tmp := embJSONRegex.ReplaceAllStringFunc(m, func(s string) string {
20 | info := &EmbeddedInfo{}
21 | if err := jsonIter.ConfigFastest.Unmarshal([]byte(s[1:]), info); err != nil || len(info.Type) == 0 || len(info.ID) == 0 {
22 | return s
23 | }
24 | res = append(res, info)
25 | if info.Type == "file" {
26 | return "[添付ファイル]"
27 | }
28 | if info.Type == "message" {
29 | return "[引用メッセージ]"
30 | }
31 | return info.Raw
32 | })
33 | return res, strings.ReplaceAll(tmp, "\n", " ")
34 | }
35 |
--------------------------------------------------------------------------------
/utils/private_ip.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "net"
4 |
5 | var privateIPBlocks []*net.IPNet
6 |
7 | func init() {
8 | for _, cidr := range []string{
9 | "127.0.0.0/8", // IPv4 loopback
10 | "10.0.0.0/8", // RFC1918
11 | "172.16.0.0/12", // RFC1918
12 | "192.168.0.0/16", // RFC1918
13 | "::1/128", // IPv6 loopback
14 | "fe80::/10", // IPv6 link-local
15 | "fc00::/7", // IPv6 unique local addr
16 | } {
17 | _, block, _ := net.ParseCIDR(cidr)
18 | privateIPBlocks = append(privateIPBlocks, block)
19 | }
20 | }
21 |
22 | // IsPrivateIP プライベートネットワーク空間のIPアドレスかどうか
23 | func IsPrivateIP(ip net.IP) bool {
24 | for _, block := range privateIPBlocks {
25 | if block.Contains(ip) {
26 | return true
27 | }
28 | }
29 | return false
30 | }
31 |
32 | // IsPrivateHost プライベートネットワーク空間のホストかどうか
33 | func IsPrivateHost(host string) bool {
34 | ips, err := net.LookupIP(host)
35 | if err != nil {
36 | return true
37 | }
38 | for _, ip := range ips {
39 | if IsPrivateIP(ip) {
40 | return true
41 | }
42 | }
43 | return false
44 | }
45 |
--------------------------------------------------------------------------------
/utils/private_ip_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "net"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestIsPrivateIP(t *testing.T) {
11 | t.Parallel()
12 | assert := assert.New(t)
13 |
14 | assert.True(IsPrivateIP(net.ParseIP("127.0.0.1")))
15 | assert.False(IsPrivateIP(net.ParseIP("8.8.8.8")))
16 | }
17 |
18 | func TestIsPrivateHost(t *testing.T) {
19 | t.Parallel()
20 | assert := assert.New(t)
21 |
22 | assert.True(IsPrivateHost("localhost"))
23 | assert.True(IsPrivateHost("127.0.0.1"))
24 | assert.True(IsPrivateHost("192.168.2.1"))
25 | assert.False(IsPrivateHost("google.com"))
26 | assert.False(IsPrivateHost("trap.jp"))
27 | assert.False(IsPrivateHost("8.8.8.8"))
28 | }
29 |
--------------------------------------------------------------------------------
/utils/random/ecdsa.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "crypto/elliptic"
6 | "crypto/rand"
7 | "crypto/x509"
8 | "encoding/pem"
9 | )
10 |
11 | // GenerateECDSAKey ECDSAによる鍵を生成します
12 | func GenerateECDSAKey() (privRaw []byte, pubRaw []byte) {
13 | priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
14 | ecder, _ := x509.MarshalECPrivateKey(priv)
15 | ecderpub, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey)
16 | return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: ecder}), pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: ecderpub})
17 | }
18 |
--------------------------------------------------------------------------------
/utils/random/random_test.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestRandAlphabetAndNumberString(t *testing.T) {
10 | t.Parallel()
11 |
12 | set := make(map[string]bool, 1000)
13 | for range 1000 {
14 | s := AlphaNumeric(10)
15 | if set[s] {
16 | t.FailNow()
17 | }
18 | set[s] = true
19 | }
20 | }
21 |
22 | func TestSecureRandAlphabetAndNumberString(t *testing.T) {
23 | t.Parallel()
24 |
25 | set := make(map[string]bool, 1000)
26 | for range 1000 {
27 | s := SecureAlphaNumeric(10)
28 | if set[s] {
29 | t.FailNow()
30 | }
31 | set[s] = true
32 | }
33 | }
34 |
35 | func TestGenerateSalt(t *testing.T) {
36 | t.Parallel()
37 |
38 | salt := Salt()
39 | assert.Len(t, salt, 64)
40 | assert.NotEqual(t, salt, Salt())
41 | }
42 |
--------------------------------------------------------------------------------
/utils/secure.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/sha512"
5 |
6 | "golang.org/x/crypto/pbkdf2"
7 | )
8 |
9 | // HashPassword パスワードをハッシュ化します
10 | func HashPassword(pass string, salt []byte) []byte {
11 | return pbkdf2.Key([]byte(pass), salt, 65536, 64, sha512.New)[:]
12 | }
13 |
--------------------------------------------------------------------------------
/utils/secure_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 |
8 | "github.com/traPtitech/traQ/utils/random"
9 | )
10 |
11 | func TestHashPassword(t *testing.T) {
12 | t.Parallel()
13 |
14 | password1 := "test"
15 | password2 := "testtest"
16 | salt1 := random.Salt()
17 | salt2 := random.Salt()
18 |
19 | assert.EqualValues(t, HashPassword(password1, salt1), HashPassword(password1, salt1))
20 | assert.NotEqual(t, HashPassword(password1, salt1), HashPassword(password1, salt2))
21 | assert.NotEqual(t, HashPassword(password2, salt1), HashPassword(password1, salt1))
22 | }
23 |
--------------------------------------------------------------------------------
/utils/set/common.go:
--------------------------------------------------------------------------------
1 | package set
2 |
3 | import jsonIter "github.com/json-iterator/go"
4 |
5 | var json = jsonIter.ConfigFastest
6 |
--------------------------------------------------------------------------------
/utils/set/set.go:
--------------------------------------------------------------------------------
1 | package set
2 |
3 | import "iter"
4 |
5 | type Set[T comparable] struct {
6 | set map[T]struct{}
7 | }
8 |
9 | func New[T comparable]() Set[T] {
10 | return Set[T]{
11 | set: make(map[T]struct{}),
12 | }
13 | }
14 |
15 | func (set *Set[T]) Add(v ...T) {
16 | for _, v := range v {
17 | set.set[v] = struct{}{}
18 | }
19 | }
20 |
21 | func (set *Set[T]) Remove(v ...T) {
22 | for _, v := range v {
23 | delete(set.set, v)
24 | }
25 | }
26 |
27 | func (set *Set[T]) Contains(v T) bool {
28 | _, ok := set.set[v]
29 | return ok
30 | }
31 |
32 | func (set *Set[T]) Len() int {
33 | return len(set.set)
34 | }
35 |
36 | func (set *Set[T]) Values() iter.Seq[T] {
37 | return func(yield func(T) bool) {
38 | for v := range set.set {
39 | if !yield(v) {
40 | return
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/utils/set/string.go:
--------------------------------------------------------------------------------
1 | package set
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // String stringの集合
8 | type String map[string]struct{}
9 |
10 | // Add 要素を追加します
11 | func (set String) Add(v ...string) {
12 | for _, v := range v {
13 | set[v] = struct{}{}
14 | }
15 | }
16 |
17 | // Remove 要素を削除します
18 | func (set String) Remove(v ...string) {
19 | for _, v := range v {
20 | delete(set, v)
21 | }
22 | }
23 |
24 | // String 要素をsep区切りで文字列に出力します
25 | func (set String) String(sep string) string {
26 | sa := make([]string, 0, len(set))
27 | for k := range set {
28 | sa = append(sa, k)
29 | }
30 | return strings.Join(sa, sep)
31 | }
32 |
33 | // Contains 指定した要素が含まれているかどうか
34 | func (set String) Contains(v string) bool {
35 | _, ok := set[v]
36 | return ok
37 | }
38 |
39 | // MarshalJSON encoding/json.Marshaler 実装
40 | func (set String) MarshalJSON() ([]byte, error) {
41 | arr := make([]string, 0, len(set))
42 | for e := range set {
43 | arr = append(arr, e)
44 | }
45 | return json.Marshal(arr)
46 | }
47 |
48 | // UnmarshalJSON encoding/json.Unmarshaler 実装
49 | func (set *String) UnmarshalJSON(data []byte) error {
50 | var value []string
51 | if err := json.Unmarshal(data, &value); err != nil {
52 | return err
53 | }
54 | *set = StringSetFromArray(value)
55 | return nil
56 | }
57 |
58 | // Clone 集合を複製します
59 | func (set String) Clone() String {
60 | a := String{}
61 | for k, v := range set {
62 | a[k] = v
63 | }
64 | return a
65 | }
66 |
67 | // StringSetFromArray 配列から集合を生成します
68 | func StringSetFromArray(arr []string) String {
69 | s := String{}
70 | s.Add(arr...)
71 | return s
72 | }
73 |
--------------------------------------------------------------------------------
/utils/storage/storage.go:
--------------------------------------------------------------------------------
1 | //go:generate mockgen -source=$GOFILE -destination=mock_$GOPACKAGE/mock_$GOFILE
2 | package storage
3 |
4 | import (
5 | "errors"
6 | "io"
7 |
8 | "github.com/traPtitech/traQ/model"
9 | )
10 |
11 | var (
12 | // ErrFileNotFound 指定されたキーのファイルは見つかりません
13 | ErrFileNotFound = errors.New("file not found")
14 | )
15 |
16 | // FileStorage ファイルストレージのインターフェース
17 | type FileStorage interface {
18 | // SaveByKey srcをkeyのファイルとして保存する
19 | SaveByKey(src io.Reader, key, name, contentType string, fileType model.FileType) error
20 | // OpenFileByKey keyで指定されたファイルを読み込む
21 | OpenFileByKey(key string, fileType model.FileType) (io.ReadSeekCloser, error)
22 | // DeleteByKey keyで指定されたファイルを削除する
23 | DeleteByKey(key string, fileType model.FileType) error
24 | // GenerateAccessURL keyで指定されたファイルの直接アクセスURLを発行する。発行機能がない場合は空文字列を返します(エラーはありません)。
25 | GenerateAccessURL(key string, fileType model.FileType) (string, error)
26 | }
27 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | func Map[T, R any](s []T, mapper func(item T) R) []R {
4 | ret := make([]R, len(s))
5 | for i := range s {
6 | ret[i] = mapper(s[i])
7 | }
8 | return ret
9 | }
10 |
11 | func MergeMap[K comparable, V any](m1, m2 map[K]V) map[K]V {
12 | ret := make(map[K]V, len(m1)+len(m2))
13 | for k, v := range m1 {
14 | ret[k] = v
15 | }
16 | for k, v := range m2 {
17 | ret[k] = v
18 | }
19 | return ret
20 | }
21 |
--------------------------------------------------------------------------------