├── .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 | --------------------------------------------------------------------------------