chore: import upstream snapshot with attribution
CI / Check and lint (push) Failing after 1s
Lint / Lint (ubuntu-latest) (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:32 +08:00
commit 39dbe3a57d
1131 changed files with 272709 additions and 0 deletions
+637
View File
@@ -0,0 +1,637 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/repo/revision"
"github.com/apache/answer/internal/repo/unique"
"github.com/apache/answer/internal/schema"
"github.com/segmentfault/pacman/log"
"github.com/apache/answer/internal/entity"
"golang.org/x/crypto/bcrypt"
"xorm.io/xorm"
)
type Mentor struct {
ctx context.Context
engine *xorm.Engine
userData *InitNeedUserInputData
err error
Done bool
}
func NewMentor(ctx context.Context, engine *xorm.Engine, data *InitNeedUserInputData) *Mentor {
return &Mentor{ctx: ctx, engine: engine, userData: data}
}
type InitNeedUserInputData struct {
Language string
SiteName string
SiteURL string
ContactEmail string
AdminName string
AdminPassword string
AdminEmail string
LoginRequired bool
ExternalContentDisplay string
}
func (m *Mentor) InitDB() error {
m.do("check table exist", m.checkTableExist)
m.do("sync table", m.syncTable)
m.do("init version table", m.initVersionTable)
m.do("init admin user", m.initAdminUser)
m.do("init config", m.initConfig)
m.do("init default privileges config", m.initDefaultRankPrivileges)
m.do("init role", m.initRole)
m.do("init power", m.initPower)
m.do("init role power rel", m.initRolePowerRel)
m.do("init admin user role rel", m.initAdminUserRoleRel)
m.do("init site info interface", m.initSiteInfoInterface)
m.do("init site info users settings", m.initSiteInfoUsersSettings)
m.do("init site info general config", m.initSiteInfoGeneralData)
m.do("init site info login config", m.initSiteInfoLoginConfig)
m.do("init site info theme config", m.initSiteInfoThemeConfig)
m.do("init site info seo config", m.initSiteInfoSEOConfig)
m.do("init site info user config", m.initSiteInfoUsersConfig)
m.do("init site info privilege rank", m.initSiteInfoPrivilegeRank)
m.do("init site info write", m.initSiteInfoAdvanced)
m.do("init site info write", m.initSiteInfoQuestions)
m.do("init site info write", m.initSiteInfoTags)
m.do("init site info security", m.initSiteInfoSecurityConfig)
m.do("init default content", m.initDefaultContent)
m.do("init default badges", m.initDefaultBadges)
m.do("init default ai config", m.initSiteInfoAI)
m.do("init default MCP config", m.initSiteInfoMCP)
return m.err
}
func (m *Mentor) do(taskName string, fn func()) {
if m.err != nil || m.Done {
return
}
fn()
if m.err != nil {
m.err = fmt.Errorf("%s failed: %s", taskName, m.err)
}
}
func (m *Mentor) checkTableExist() {
m.Done, m.err = m.engine.Context(m.ctx).IsTableExist(&entity.Version{})
if m.Done {
fmt.Println("[database] already exists")
}
}
func (m *Mentor) syncTable() {
m.err = m.engine.Context(m.ctx).Sync(tables...)
}
func (m *Mentor) initVersionTable() {
_, m.err = m.engine.Context(m.ctx).Insert(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()})
}
func (m *Mentor) initAdminUser() {
generateFromPassword, _ := bcrypt.GenerateFromPassword([]byte(m.userData.AdminPassword), bcrypt.DefaultCost)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.User{
ID: "1",
Username: m.userData.AdminName,
Pass: string(generateFromPassword),
EMail: m.userData.AdminEmail,
MailStatus: 1,
NoticeStatus: 1,
Status: 1,
Rank: 1,
DisplayName: m.userData.AdminName,
})
}
func (m *Mentor) initConfig() {
_, m.err = m.engine.Context(m.ctx).Insert(defaultConfigTable)
}
func (m *Mentor) initDefaultRankPrivileges() {
chooseOption := schema.DefaultPrivilegeOptions.Choose(schema.PrivilegeLevel2)
for _, privilege := range chooseOption.Privileges {
_, err := m.engine.Context(m.ctx).Update(
&entity.Config{Value: fmt.Sprintf("%d", privilege.Value)},
&entity.Config{Key: privilege.Key},
)
if err != nil {
log.Error(err)
}
}
}
func (m *Mentor) initRole() {
_, m.err = m.engine.Context(m.ctx).Insert(roles)
}
func (m *Mentor) initPower() {
_, m.err = m.engine.Context(m.ctx).Insert(powers)
}
func (m *Mentor) initRolePowerRel() {
_, m.err = m.engine.Context(m.ctx).Insert(rolePowerRels)
}
func (m *Mentor) initAdminUserRoleRel() {
_, m.err = m.engine.Context(m.ctx).Insert(adminUserRoleRel)
}
func (m *Mentor) initSiteInfoInterface() {
now := time.Now()
zoneName, offset := now.In(time.Local).Zone()
localTimezone := "UTC"
for _, tz := range constant.Timezones {
loc, err := time.LoadLocation(tz)
if err != nil {
continue
}
tzNow := now.In(loc)
tzName, tzOffset := tzNow.Zone()
if tzName == zoneName && tzOffset == offset {
localTimezone = tz
break
}
}
interfaceData := map[string]string{
"language": m.userData.Language,
"time_zone": localTimezone,
}
interfaceDataBytes, _ := json.Marshal(interfaceData)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "interface_settings",
Content: string(interfaceDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoUsersSettings() {
usersSettings := map[string]any{
"default_avatar": "gravatar",
"gravatar_base_url": "https://www.gravatar.com/avatar/",
}
usersSettingsDataBytes, _ := json.Marshal(usersSettings)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "users_settings",
Content: string(usersSettingsDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoGeneralData() {
generalData := map[string]string{
"name": m.userData.SiteName,
"site_url": m.userData.SiteURL,
"contact_email": m.userData.ContactEmail,
}
generalDataBytes, _ := json.Marshal(generalData)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "general",
Content: string(generalDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoLoginConfig() {
loginConfig := map[string]any{
"allow_new_registrations": true,
"allow_email_registrations": true,
"allow_password_login": true,
"require_email_verification": true,
}
loginConfigDataBytes, _ := json.Marshal(loginConfig)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "login",
Content: string(loginConfigDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoSecurityConfig() {
securityConfig := map[string]any{
"login_required": m.userData.LoginRequired,
"external_content_display": m.userData.ExternalContentDisplay,
"check_update": true,
}
securityConfigDataBytes, _ := json.Marshal(securityConfig)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "security",
Content: string(securityConfigDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoThemeConfig() {
themeConfig := fmt.Sprintf(`{"theme":"default","theme_config":{"default":{"navbar_style":"#0033ff","primary_color":"#0033ff"}},"layout":"%s"}`, constant.ThemeLayoutFullWidth)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "theme",
Content: themeConfig,
Status: 1,
})
}
func (m *Mentor) initSiteInfoSEOConfig() {
seoData := map[string]any{
"permalink": constant.PermalinkQuestionID,
"robots": defaultSEORobotTxt + m.userData.SiteURL + "/sitemap.xml",
}
seoDataBytes, _ := json.Marshal(seoData)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "seo",
Content: string(seoDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoUsersConfig() {
usersData := map[string]any{
"default_avatar": "gravatar",
"gravatar_base_url": "https://www.gravatar.com/avatar/",
"allow_update_display_name": true,
"allow_update_username": true,
"allow_update_avatar": true,
"allow_update_bio": true,
"allow_update_website": true,
"allow_update_location": true,
}
usersDataBytes, _ := json.Marshal(usersData)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "users",
Content: string(usersDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoPrivilegeRank() {
privilegeRankData := map[string]any{
"level": schema.PrivilegeLevel2,
}
privilegeRankDataBytes, _ := json.Marshal(privilegeRankData)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "privileges",
Content: string(privilegeRankDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoAdvanced() {
advancedData := map[string]any{
"max_image_size": 4,
"max_attachment_size": 8,
"max_image_megapixel": 40,
"authorized_image_extensions": []string{"jpg", "jpeg", "png", "gif", "webp"},
"authorized_attachment_extensions": []string{},
}
advancedDataBytes, _ := json.Marshal(advancedData)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "advanced",
Content: string(advancedDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoQuestions() {
questionsData := map[string]any{
"min_tags": 1,
"min_content": 6,
"restrict_answer": true,
}
questionsDataBytes, _ := json.Marshal(questionsData)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "questions",
Content: string(questionsDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoTags() {
tagsData := map[string]any{
"required_tag": false,
"recommend_tags": []string{},
"reserved_tags": []string{},
}
tagsDataBytes, _ := json.Marshal(tagsData)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "tags",
Content: string(tagsDataBytes),
Status: 1,
})
}
func (m *Mentor) initDefaultContent() {
uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine})
revisionRepo := revision.NewRevisionRepo(&data.Data{DB: m.engine}, uniqueIDRepo)
now := time.Now()
tagId, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Tag{}.TableName())
if err != nil {
m.err = err
return
}
q1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName())
if err != nil {
m.err = err
return
}
a1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName())
if err != nil {
m.err = err
return
}
q2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName())
if err != nil {
m.err = err
return
}
a2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName())
if err != nil {
m.err = err
return
}
tag := &entity.Tag{
ID: tagId,
SlugName: "support",
DisplayName: "support",
OriginalText: "For general support questions.",
ParsedText: "<p>For general support questions.</p>",
UserID: "1",
QuestionCount: 2,
Status: entity.TagStatusAvailable,
RevisionID: "0",
}
q1 := &entity.Question{
ID: q1Id,
CreatedAt: now,
UserID: "1",
LastEditUserID: "1",
Title: "What is a tag?",
OriginalText: "When asking a question, we need to choose tags. What are tags and why should I use them?",
ParsedText: "<p>When asking a question, we need to choose tags. What are tags and why should I use them?</p>",
Pin: entity.QuestionUnPin,
Show: entity.QuestionShow,
Status: entity.QuestionStatusAvailable,
AnswerCount: 1,
AcceptedAnswerID: "0",
LastAnswerID: a1Id,
PostUpdateTime: now,
RevisionID: "0",
}
a1 := &entity.Answer{
ID: a1Id,
CreatedAt: now,
QuestionID: q1Id,
UserID: "1",
LastEditUserID: "0",
OriginalText: "Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.",
ParsedText: "<p>Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.</p>",
Status: entity.AnswerStatusAvailable,
RevisionID: "0",
}
q2 := &entity.Question{
ID: q2Id,
CreatedAt: now,
UserID: "1",
LastEditUserID: "1",
Title: "What is reputation and how do I earn them?",
OriginalText: "I see that each user has reputation points, What is it and how do I earn them?",
ParsedText: "<p>I see that each user has reputation points, What is it and how do I earn them?</p>",
Pin: entity.QuestionUnPin,
Show: entity.QuestionShow,
Status: entity.QuestionStatusAvailable,
AnswerCount: 1,
AcceptedAnswerID: "0",
LastAnswerID: a2Id,
PostUpdateTime: now,
RevisionID: "0",
}
a2 := &entity.Answer{
ID: a2Id,
CreatedAt: now,
QuestionID: q2Id,
UserID: "1",
LastEditUserID: "0",
OriginalText: "Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.",
ParsedText: "<p>Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.</p>",
Status: entity.AnswerStatusAvailable,
RevisionID: "0",
}
_, m.err = m.engine.Context(m.ctx).Insert(tag)
if m.err != nil {
return
}
tagContent, err := json.Marshal(tag)
if err != nil {
m.err = err
return
}
m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{
UserID: tag.UserID,
ObjectID: tag.ID,
Title: tag.SlugName,
Content: string(tagContent),
Status: entity.RevisionReviewPassStatus,
}, true)
if m.err != nil {
return
}
tagForRevision := &entity.TagSimpleInfoForRevision{
ID: tag.ID,
MainTagID: tag.MainTagID,
MainTagSlugName: tag.MainTagSlugName,
SlugName: tag.SlugName,
DisplayName: tag.DisplayName,
Recommend: tag.Recommend,
Reserved: tag.Reserved,
RevisionID: tag.RevisionID,
}
_, m.err = m.engine.Context(m.ctx).Insert(q1)
if m.err != nil {
return
}
q1Revision := &entity.QuestionWithTagsRevision{
Question: *q1,
Tags: []*entity.TagSimpleInfoForRevision{tagForRevision},
}
q1Content, err := json.Marshal(q1Revision)
if err != nil {
m.err = err
return
}
m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{
UserID: q1.UserID,
ObjectID: q1.ID,
Title: q1.Title,
Content: string(q1Content),
Status: entity.RevisionReviewPassStatus,
}, true)
if m.err != nil {
return
}
_, m.err = m.engine.Context(m.ctx).Insert(a1)
if m.err != nil {
return
}
a1Content, err := json.Marshal(a1)
if err != nil {
m.err = err
return
}
m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{
UserID: a1.UserID,
ObjectID: a1.ID,
Content: string(a1Content),
Status: entity.RevisionReviewPassStatus,
}, true)
if m.err != nil {
return
}
_, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{
ObjectID: q1.ID,
TagID: tag.ID,
Status: entity.TagRelStatusAvailable,
})
if m.err != nil {
return
}
_, m.err = m.engine.Context(m.ctx).Insert(q2)
if m.err != nil {
return
}
q2Revision := &entity.QuestionWithTagsRevision{
Question: *q2,
Tags: []*entity.TagSimpleInfoForRevision{tagForRevision},
}
q2Content, err := json.Marshal(q2Revision)
if err != nil {
m.err = err
return
}
m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{
UserID: q2.UserID,
ObjectID: q2.ID,
Title: q2.Title,
Content: string(q2Content),
Status: entity.RevisionReviewPassStatus,
}, true)
if m.err != nil {
return
}
_, m.err = m.engine.Context(m.ctx).Insert(a2)
if m.err != nil {
return
}
a2Content, err := json.Marshal(a2)
if err != nil {
m.err = err
return
}
m.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{
UserID: a2.UserID,
ObjectID: a2.ID,
Content: string(a2Content),
Status: entity.RevisionReviewPassStatus,
}, true)
if m.err != nil {
return
}
_, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{
ObjectID: q2.ID,
TagID: tag.ID,
Status: entity.TagRelStatusAvailable,
})
if m.err != nil {
return
}
}
func (m *Mentor) initDefaultBadges() {
uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine})
_, m.err = m.engine.Context(m.ctx).Insert(defaultBadgeGroupTable)
if m.err != nil {
return
}
for _, badge := range defaultBadgeTable {
badge.ID, m.err = uniqueIDRepo.GenUniqueIDStr(m.ctx, new(entity.Badge).TableName())
if m.err != nil {
return
}
if _, m.err = m.engine.Context(m.ctx).Insert(badge); m.err != nil {
return
}
}
}
func (m *Mentor) initSiteInfoAI() {
content := &schema.SiteAIReq{
PromptConfig: &schema.AIPromptConfig{
ZhCN: constant.DefaultAIPromptConfigZhCN,
EnUS: constant.DefaultAIPromptConfigEnUS,
},
}
writeDataBytes, _ := json.Marshal(content)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypeAI,
Content: string(writeDataBytes),
Status: 1,
})
}
func (m *Mentor) initSiteInfoMCP() {
content := &schema.SiteMCPReq{
Enabled: true,
}
writeDataBytes, _ := json.Marshal(content)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypeMCP,
Content: string(writeDataBytes),
Status: 1,
})
}
+514
View File
@@ -0,0 +1,514 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/permission"
)
const (
defaultSEORobotTxt = `User-agent: *
Disallow: /admin
Disallow: /search
Disallow: /install
Disallow: /review
Disallow: /users/login
Disallow: /users/register
Disallow: /users/account-recovery
Disallow: /users/oauth/*
Disallow: /users/*/*
Disallow: /answer/api
Disallow: /*?code*
Disallow: /swagger/*
Sitemap: `
)
var (
tables = []any{
&entity.Activity{},
&entity.Answer{},
&entity.Collection{},
&entity.CollectionGroup{},
&entity.Comment{},
&entity.Config{},
&entity.Meta{},
&entity.Notification{},
&entity.Question{},
&entity.QuestionLink{},
&entity.Report{},
&entity.Revision{},
&entity.SiteInfo{},
&entity.Tag{},
&entity.TagRel{},
&entity.Uniqid{},
&entity.User{},
&entity.Version{},
&entity.Role{},
&entity.RolePowerRel{},
&entity.Power{},
&entity.UserRoleRel{},
&entity.PluginConfig{},
&entity.UserExternalLogin{},
&entity.UserNotificationConfig{},
&entity.PluginUserConfig{},
&entity.Review{},
&entity.Badge{},
&entity.BadgeGroup{},
&entity.BadgeAward{},
&entity.FileRecord{},
&entity.PluginKVStorage{},
&entity.APIKey{},
&entity.AIConversation{},
&entity.AIConversationRecord{},
}
roles = []*entity.Role{
{ID: 1, Name: "User", Description: "Default with no special access."},
{ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
{ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
}
powers = []*entity.Power{
{ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
{ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
{ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
{ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
{ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
{ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
{ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
{ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
{ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
{ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
{ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
{ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
{ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
{ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
{ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
{ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
{ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
{ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
{ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
{ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
{ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
{ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
{ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
{ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
{ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
{ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
{ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
{ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
{ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
{ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
{ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
{ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
{ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
{ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"},
{ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"},
{ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"},
{ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"},
{ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"},
{ID: 39, Name: "recover answer", PowerType: permission.AnswerUnDelete, Description: "recover deleted answer"},
{ID: 40, Name: "recover question", PowerType: permission.QuestionUnDelete, Description: "recover deleted question"},
{ID: 41, Name: "recover tag", PowerType: permission.TagUnDelete, Description: "recover deleted tag"},
}
rolePowerRels = []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AdminAccess},
{RoleID: 2, PowerType: permission.QuestionAdd},
{RoleID: 2, PowerType: permission.QuestionEdit},
{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 2, PowerType: permission.QuestionDelete},
{RoleID: 2, PowerType: permission.QuestionClose},
{RoleID: 2, PowerType: permission.QuestionReopen},
{RoleID: 2, PowerType: permission.QuestionVoteUp},
{RoleID: 2, PowerType: permission.QuestionVoteDown},
{RoleID: 2, PowerType: permission.AnswerAdd},
{RoleID: 2, PowerType: permission.AnswerEdit},
{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 2, PowerType: permission.AnswerDelete},
{RoleID: 2, PowerType: permission.AnswerAccept},
{RoleID: 2, PowerType: permission.AnswerVoteUp},
{RoleID: 2, PowerType: permission.AnswerVoteDown},
{RoleID: 2, PowerType: permission.CommentAdd},
{RoleID: 2, PowerType: permission.CommentEdit},
{RoleID: 2, PowerType: permission.CommentDelete},
{RoleID: 2, PowerType: permission.CommentVoteUp},
{RoleID: 2, PowerType: permission.CommentVoteDown},
{RoleID: 2, PowerType: permission.ReportAdd},
{RoleID: 2, PowerType: permission.TagAdd},
{RoleID: 2, PowerType: permission.TagEdit},
{RoleID: 2, PowerType: permission.TagEditSlugName},
{RoleID: 2, PowerType: permission.TagEditWithoutReview},
{RoleID: 2, PowerType: permission.TagDelete},
{RoleID: 2, PowerType: permission.TagSynonym},
{RoleID: 2, PowerType: permission.LinkUrlLimit},
{RoleID: 2, PowerType: permission.VoteDetail},
{RoleID: 2, PowerType: permission.AnswerAudit},
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 2, PowerType: permission.QuestionPin},
{RoleID: 2, PowerType: permission.QuestionHide},
{RoleID: 2, PowerType: permission.QuestionUnPin},
{RoleID: 2, PowerType: permission.QuestionShow},
{RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer},
{RoleID: 2, PowerType: permission.AnswerUnDelete},
{RoleID: 2, PowerType: permission.QuestionUnDelete},
{RoleID: 2, PowerType: permission.TagUnDelete},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 3, PowerType: permission.QuestionDelete},
{RoleID: 3, PowerType: permission.QuestionClose},
{RoleID: 3, PowerType: permission.QuestionReopen},
{RoleID: 3, PowerType: permission.QuestionVoteUp},
{RoleID: 3, PowerType: permission.QuestionVoteDown},
{RoleID: 3, PowerType: permission.AnswerAdd},
{RoleID: 3, PowerType: permission.AnswerEdit},
{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 3, PowerType: permission.AnswerDelete},
{RoleID: 3, PowerType: permission.AnswerAccept},
{RoleID: 3, PowerType: permission.AnswerVoteUp},
{RoleID: 3, PowerType: permission.AnswerVoteDown},
{RoleID: 3, PowerType: permission.CommentAdd},
{RoleID: 3, PowerType: permission.CommentEdit},
{RoleID: 3, PowerType: permission.CommentDelete},
{RoleID: 3, PowerType: permission.CommentVoteUp},
{RoleID: 3, PowerType: permission.CommentVoteDown},
{RoleID: 3, PowerType: permission.ReportAdd},
{RoleID: 3, PowerType: permission.TagAdd},
{RoleID: 3, PowerType: permission.TagEdit},
{RoleID: 3, PowerType: permission.TagEditSlugName},
{RoleID: 3, PowerType: permission.TagEditWithoutReview},
{RoleID: 3, PowerType: permission.TagDelete},
{RoleID: 3, PowerType: permission.TagSynonym},
{RoleID: 3, PowerType: permission.LinkUrlLimit},
{RoleID: 3, PowerType: permission.VoteDetail},
{RoleID: 3, PowerType: permission.AnswerAudit},
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionPin},
{RoleID: 3, PowerType: permission.QuestionHide},
{RoleID: 3, PowerType: permission.QuestionUnPin},
{RoleID: 3, PowerType: permission.QuestionShow},
{RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer},
{RoleID: 3, PowerType: permission.AnswerUnDelete},
{RoleID: 3, PowerType: permission.QuestionUnDelete},
{RoleID: 3, PowerType: permission.TagUnDelete},
}
adminUserRoleRel = &entity.UserRoleRel{
UserID: "1",
RoleID: 2,
}
defaultConfigTable = []*entity.Config{
{ID: 1, Key: "answer.accepted", Value: `15`},
{ID: 2, Key: "answer.voted_up", Value: `10`},
{ID: 3, Key: "question.voted_up", Value: `10`},
{ID: 4, Key: "tag.edit_accepted", Value: `2`},
{ID: 5, Key: "answer.accept", Value: `2`},
{ID: 6, Key: "answer.voted_down_cancel", Value: `2`},
{ID: 7, Key: "question.voted_down_cancel", Value: `2`},
{ID: 8, Key: "answer.vote_down_cancel", Value: `1`},
{ID: 9, Key: "question.vote_down_cancel", Value: `1`},
{ID: 10, Key: "user.activated", Value: `1`},
{ID: 11, Key: "edit.accepted", Value: `2`},
{ID: 12, Key: "answer.vote_down", Value: `-1`},
{ID: 13, Key: "question.voted_down", Value: `-2`},
{ID: 14, Key: "answer.voted_down", Value: `-2`},
{ID: 15, Key: "answer.accept_cancel", Value: `-2`},
{ID: 16, Key: "answer.deleted", Value: `-5`},
{ID: 17, Key: "question.voted_up_cancel", Value: `-10`},
{ID: 18, Key: "answer.voted_up_cancel", Value: `-10`},
{ID: 19, Key: "answer.accepted_cancel", Value: `-15`},
{ID: 20, Key: "object.reported", Value: `-100`},
{ID: 21, Key: "edit.rejected", Value: `-2`},
{ID: 22, Key: "daily_rank_limit", Value: `200`},
{ID: 23, Key: "daily_rank_limit.exclude", Value: `["answer.accepted"]`},
{ID: 24, Key: "user.follow", Value: `0`},
{ID: 25, Key: "comment.vote_up", Value: `0`},
{ID: 26, Key: "comment.vote_up_cancel", Value: `0`},
{ID: 27, Key: "question.vote_down", Value: `0`},
{ID: 28, Key: "question.vote_up", Value: `0`},
{ID: 29, Key: "question.vote_up_cancel", Value: `0`},
{ID: 30, Key: "answer.vote_up", Value: `0`},
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
{ID: 32, Key: "question.follow", Value: `0`},
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"}`},
{ID: 35, Key: "tag.follow", Value: `0`},
{ID: 36, Key: "rank.question.add", Value: `1`},
{ID: 37, Key: "rank.question.edit", Value: `200`},
{ID: 38, Key: "rank.question.delete", Value: `-1`},
{ID: 39, Key: "rank.question.vote_up", Value: `15`},
{ID: 40, Key: "rank.question.vote_down", Value: `125`},
{ID: 41, Key: "rank.answer.add", Value: `1`},
{ID: 42, Key: "rank.answer.edit", Value: `200`},
{ID: 43, Key: "rank.answer.delete", Value: `-1`},
{ID: 44, Key: "rank.answer.accept", Value: `-1`},
{ID: 45, Key: "rank.answer.vote_up", Value: `15`},
{ID: 46, Key: "rank.answer.vote_down", Value: `125`},
{ID: 47, Key: "rank.comment.add", Value: `1`},
{ID: 48, Key: "rank.comment.edit", Value: `-1`},
{ID: 49, Key: "rank.comment.delete", Value: `-1`},
{ID: 50, Key: "rank.report.add", Value: `1`},
{ID: 51, Key: "rank.tag.add", Value: `1500`},
{ID: 52, Key: "rank.tag.edit", Value: `100`},
{ID: 53, Key: "rank.tag.delete", Value: `-1`},
{ID: 54, Key: "rank.tag.synonym", Value: `20000`},
{ID: 55, Key: "rank.link.url_limit", Value: `10`},
{ID: 56, Key: "rank.vote.detail", Value: `0`},
{ID: 57, Key: "reason.spam", Value: `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`},
{ID: 58, Key: "reason.rude_or_abusive", Value: `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`},
{ID: 59, Key: "reason.something", Value: `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`},
{ID: 60, Key: "reason.a_duplicate", Value: `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`},
{ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`},
{ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`},
{ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesn't meet a community guideline."}`},
{ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only."}`},
{ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`},
{ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`},
{ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question can't answer, but still can edit, vote and comment."}`},
{ID: 68, Key: "reason.deleted", Value: `{"name":"deleted","description":"All reputation gained and lost will be restored."}`},
{ID: 69, Key: "reason.deleted.user", Value: `{"name":"deleted","description":"Delete profile, authentication associations."}`},
{ID: 70, Key: "reason.suspended", Value: `{"name":"suspended","description":"A suspended user can't log in."}`},
{ID: 71, Key: "reason.inactive", Value: `{"name":"inactive","description":"An inactive user must re-validate their email."}`},
{ID: 72, Key: "reason.looks_ok", Value: `{"name":"looks ok","description":"This post is good as-is and not low quality."}`},
{ID: 73, Key: "reason.needs_edit", Value: `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`},
{ID: 74, Key: "reason.needs_close", Value: `{"name":"needs close","description":"A closed question can't answer, but still can edit, vote and comment."}`},
{ID: 75, Key: "reason.needs_delete", Value: `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`},
{ID: 76, Key: "question.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`},
{ID: 77, Key: "answer.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`},
{ID: 78, Key: "comment.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`},
{ID: 79, Key: "question.close.reasons", Value: `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`},
{ID: 80, Key: "question.status.reasons", Value: `["reason.normal","reason.closed","reason.deleted"]`},
{ID: 81, Key: "answer.status.reasons", Value: `["reason.normal","reason.deleted"]`},
{ID: 82, Key: "comment.status.reasons", Value: `["reason.normal","reason.deleted"]`},
{ID: 83, Key: "user.status.reasons", Value: `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`},
{ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`},
{ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 87, Key: "question.asked", Value: `0`},
{ID: 88, Key: "question.closed", Value: `0`},
{ID: 89, Key: "question.reopened", Value: `0`},
{ID: 90, Key: "question.answered", Value: `0`},
{ID: 91, Key: "question.commented", Value: `0`},
{ID: 92, Key: "question.accept", Value: `0`},
{ID: 93, Key: "question.edited", Value: `0`},
{ID: 94, Key: "question.rollback", Value: `0`},
{ID: 95, Key: "question.deleted", Value: `0`},
{ID: 96, Key: "question.undeleted", Value: `0`},
{ID: 97, Key: "answer.answered", Value: `0`},
{ID: 98, Key: "answer.commented", Value: `0`},
{ID: 99, Key: "answer.edited", Value: `0`},
{ID: 100, Key: "answer.rollback", Value: `0`},
{ID: 101, Key: "answer.undeleted", Value: `0`},
{ID: 102, Key: "tag.created", Value: `0`},
{ID: 103, Key: "tag.edited", Value: `0`},
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
{ID: 107, Key: "rank.comment.vote_up", Value: `1`},
{ID: 108, Key: "rank.comment.vote_down", Value: `1`},
{ID: 109, Key: "rank.question.edit_without_review", Value: `2000`},
{ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`},
{ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`},
{ID: 112, Key: "rank.answer.audit", Value: `2000`},
{ID: 113, Key: "rank.question.audit", Value: `2000`},
{ID: 114, Key: "rank.tag.audit", Value: `20000`},
{ID: 115, Key: "rank.question.close", Value: `-1`},
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
{ID: 118, Key: "plugin.status", Value: `{}`},
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
{ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`},
{ID: 128, Key: "rank.answer.undeleted", Value: `-1`},
{ID: 129, Key: "rank.question.undeleted", Value: `-1`},
{ID: 130, Key: "rank.tag.undeleted", Value: `-1`},
{ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"}]`},
}
defaultBadgeGroupTable = []*entity.BadgeGroup{
{ID: "1", Name: "badge.default_badge_groups.getting_started.name"},
{ID: "2", Name: "badge.default_badge_groups.community.name"},
{ID: "3", Name: "badge.default_badge_groups.posting.name"},
}
defaultBadgeTable = []*entity.Badge{
{
Name: "badge.default_badges.autobiographer.name",
Icon: "person-badge-fill",
Description: "badge.default_badges.autobiographer.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 1,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeSingleAward,
Handler: "FirstUpdateUserProfile",
},
{
Name: "badge.default_badges.editor.name",
Icon: "pencil-fill",
Description: "badge.default_badges.editor.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 1,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeSingleAward,
Handler: "FirstPostEdit",
},
{
Name: "badge.default_badges.first_flag.name",
Icon: "flag-fill",
Description: "badge.default_badges.first_flag.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 1,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeSingleAward,
Handler: "FirstFlaggedPost",
},
{
Name: "badge.default_badges.first_upvote.name",
Icon: "hand-thumbs-up-fill",
Description: "badge.default_badges.first_upvote.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 1,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeSingleAward,
Handler: "FirstVotedPost",
},
{
Name: "badge.default_badges.first_reaction.name",
Icon: "emoji-smile-fill",
Description: "badge.default_badges.first_reaction.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 1,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeSingleAward,
Handler: "FirstReactedPost",
},
{
Name: "badge.default_badges.first_share.name",
Icon: "share-fill",
Description: "badge.default_badges.first_share.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 1,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeSingleAward,
Handler: "FirstSharedPost",
},
{
Name: "badge.default_badges.scholar.name",
Icon: "check-circle-fill",
Description: "badge.default_badges.scholar.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 1,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeSingleAward,
Handler: "FirstAcceptAnswer",
},
{
Name: "badge.default_badges.solved.name",
Icon: "check-square-fill",
Description: "badge.default_badges.solved.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 2,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeSingleAward,
Handler: "ReachAnswerAcceptedAmount",
Param: `{"amount":"1"}`,
},
{
Name: "badge.default_badges.nice_answer.name",
Icon: "chat-square-text-fill",
Description: "badge.default_badges.nice_answer.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 3,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeMultiAward,
Handler: "ReachAnswerVote",
Param: `{"amount":"10"}`,
},
{
Name: "badge.default_badges.good_answer.name",
Icon: "chat-square-text-fill",
Description: "badge.default_badges.good_answer.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 3,
Level: entity.BadgeLevelSilver,
Single: entity.BadgeMultiAward,
Handler: "ReachAnswerVote",
Param: `{"amount":"25"}`,
},
{
Name: "badge.default_badges.great_answer.name",
Icon: "chat-square-text-fill",
Description: "badge.default_badges.great_answer.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 3,
Level: entity.BadgeLevelGold,
Single: entity.BadgeMultiAward,
Handler: "ReachAnswerVote",
Param: `{"amount":"50"}`,
},
{
Name: "badge.default_badges.nice_question.name",
Icon: "question-circle-fill",
Description: "badge.default_badges.nice_question.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 3,
Level: entity.BadgeLevelBronze,
Single: entity.BadgeMultiAward,
Handler: "ReachQuestionVote",
Param: `{"amount":"10"}`,
},
{
Name: "badge.default_badges.good_question.name",
Icon: "question-circle-fill",
Description: "badge.default_badges.good_question.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 3,
Level: entity.BadgeLevelSilver,
Single: entity.BadgeMultiAward,
Handler: "ReachQuestionVote",
Param: `{"amount":"25"}`,
},
{
Name: "badge.default_badges.great_question.name",
Icon: "question-circle-fill",
Description: "badge.default_badges.great_question.desc",
Status: entity.BadgeStatusActive,
BadgeGroupID: 3,
Level: entity.BadgeLevelGold,
Single: entity.BadgeMultiAward,
Handler: "ReachQuestionVote",
Param: `{"amount":"50"}`,
},
}
)
+200
View File
@@ -0,0 +1,200 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
const minDBVersion = 0
// Migration describes on migration from lower version to high version
type Migration interface {
Version() string
Description() string
Migrate(ctx context.Context, x *xorm.Engine) error
ShouldCleanCache() bool
}
type migration struct {
version string
description string
migrate func(ctx context.Context, x *xorm.Engine) error
shouldCleanCache bool
}
// Version returns the migration's version
func (m *migration) Version() string {
return m.version
}
// Description returns the migration's description
func (m *migration) Description() string {
return m.description
}
// Migrate executes the migration
func (m *migration) Migrate(ctx context.Context, x *xorm.Engine) error {
return m.migrate(ctx, x)
}
// ShouldCleanCache should clean the cache
func (m *migration) ShouldCleanCache() bool {
return m.shouldCleanCache
}
// NewMigration creates a new migration
func NewMigration(version, desc string, fn func(ctx context.Context, x *xorm.Engine) error, shouldCleanCache bool) Migration {
return &migration{version: version, description: desc, migrate: fn, shouldCleanCache: shouldCleanCache}
}
// Use noopMigration when there is a migration that has been no-oped
var noopMigration = func(_ context.Context, _ *xorm.Engine) error { return nil }
var migrations = []Migration{
// 0->1
NewMigration("v0.0.1", "this is first version, no operation", noopMigration, false),
NewMigration("v0.3.0", "add user language", addUserLanguage, false),
NewMigration("v0.4.1", "add recommend and reserved tag fields", addTagRecommendedAndReserved, false),
NewMigration("v0.5.0", "add activity timeline", addActivityTimeline, false),
NewMigration("v0.6.0", "add user role", addRoleFeatures, false),
NewMigration("v1.0.0", "add theme and private mode", addThemeAndPrivateMode, true),
NewMigration("v1.0.2", "add new answer notification", addNewAnswerNotification, true),
NewMigration("v1.0.5", "add plugin", addPlugin, false),
NewMigration("v1.0.7", "add user pin hide features", addRolePinAndHideFeatures, true),
NewMigration("v1.0.8", "update accept answer rank", updateAcceptAnswerRank, true),
NewMigration("v1.0.9", "add login limitations", addLoginLimitations, true),
NewMigration("v1.1.0-beta.1", "update user pin hide features", updateRolePinAndHideFeatures, true),
NewMigration("v1.1.0-beta.2", "update question post time", updateQuestionPostTime, true),
NewMigration("v1.1.0", "add gravatar base url", updateCount, true),
NewMigration("v1.1.1", "update the length of revision content", updateTheLengthOfRevisionContent, false),
NewMigration("v1.1.2", "add notification config", addNoticeConfig, true),
NewMigration("v1.1.3", "set default user notification config", setDefaultUserNotificationConfig, false),
NewMigration("v1.2.0", "add recover answer permission", addRecoverPermission, true),
NewMigration("v1.2.1", "add password login control", addPasswordLoginControl, true),
NewMigration("v1.2.5", "add notification plugin and theme config", addNotificationPluginAndThemeConfig, true),
NewMigration("v1.3.0", "add review", addReview, false),
NewMigration("v1.3.6", "add hot score to question table", addQuestionHotScore, true),
NewMigration("v1.4.0", "add badge/badge_group/badge_award table", addBadges, true),
NewMigration("v1.4.1", "add question link", addQuestionLink, true),
NewMigration("v1.4.2", "add the number of question links", addQuestionLinkedCount, true),
NewMigration("v1.4.5", "add file record", addFileRecord, true),
NewMigration("v1.5.1", "add plugin kv storage", addPluginKVStorage, true),
NewMigration("v1.6.0", "move user config to interface", moveUserConfigToInterface, true),
NewMigration("v1.7.0", "add optional tags", addOptionalTags, true),
NewMigration("v1.7.2", "expand avatar column length", expandAvatarColumnLength, false),
NewMigration("v1.8.0", "change admin menu", updateAdminMenuSettings, true),
NewMigration("v1.8.1", "ai feat", aiFeat, true),
NewMigration("v2.0.1", "change avatar type to text", updateAvatarType, false),
NewMigration("v2.0.2", "add reasoning content to ai conversation record", addAIConversationReasoningContent, false),
NewMigration("v2.0.3", "add require email verification login setting", addRequireEmailVerification, true),
}
func GetMigrations() []Migration {
return migrations
}
// GetCurrentDBVersion returns the current db version
func GetCurrentDBVersion(engine *xorm.Engine) (int64, error) {
if err := engine.Sync(new(entity.Version)); err != nil {
return -1, fmt.Errorf("sync version failed: %v", err)
}
currentVersion := &entity.Version{ID: 1}
has, err := engine.Get(currentVersion)
if err != nil {
return -1, fmt.Errorf("get first version failed: %v", err)
}
if !has {
_, err := engine.InsertOne(&entity.Version{ID: 1, VersionNumber: 0})
if err != nil {
return -1, fmt.Errorf("insert first version failed: %v", err)
}
return 0, nil
}
return currentVersion.VersionNumber, nil
}
// ExpectedVersion returns the expected db version
func ExpectedVersion() int64 {
return int64(minDBVersion + len(migrations))
}
// Migrate database to current version
func Migrate(debug bool, dbConf *data.Database, cacheConf *data.CacheConf, upgradeToSpecificVersion string) error {
cache, cacheCleanup, err := data.NewCache(cacheConf)
if err != nil {
fmt.Println("new cache failed:", err.Error())
}
engine, err := data.NewDB(debug, dbConf)
if err != nil {
fmt.Println("new database failed: ", err.Error())
return err
}
defer func() {
_ = engine.Close()
}()
currentDBVersion, err := GetCurrentDBVersion(engine)
if err != nil {
return err
}
expectedVersion := ExpectedVersion()
if len(upgradeToSpecificVersion) > 0 {
fmt.Printf("[migrate] user set upgrade to version: %s\n", upgradeToSpecificVersion)
for i, m := range migrations {
if m.Version() == upgradeToSpecificVersion {
currentDBVersion = int64(i)
break
}
}
}
for currentDBVersion < expectedVersion {
fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n",
currentDBVersion, currentDBVersion+1, expectedVersion)
migrationFunc := migrations[currentDBVersion]
fmt.Printf("[migrate] try to migrate Answer version %s, description: %s\n", migrationFunc.Version(), migrationFunc.Description())
if err := migrationFunc.Migrate(context.Background(), engine); err != nil {
fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error())
return err
}
if migrationFunc.ShouldCleanCache() {
if err := cache.Flush(context.Background()); err != nil {
fmt.Printf("[migrate] flush cache failed: %s\n", err.Error())
}
}
fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1)
if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {
fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
return err
}
currentDBVersion++
}
if cache != nil {
cacheCleanup()
}
return nil
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"xorm.io/xorm"
)
func addUserLanguage(ctx context.Context, x *xorm.Engine) error {
type User struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
Username string `xorm:"not null default '' VARCHAR(50) UNIQUE username"`
Language string `xorm:"not null default '' VARCHAR(100) language"`
}
return x.Context(ctx).Sync(new(User))
}
+91
View File
@@ -0,0 +1,91 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/tidwall/gjson"
"xorm.io/xorm"
)
func addLoginLimitations(ctx context.Context, x *xorm.Engine) error {
loginSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeLogin,
}
exist, err := x.Context(ctx).Get(loginSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
content := &schema.SiteLoginReq{}
_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
content.AllowEmailRegistrations = true
content.AllowEmailDomains = make([]string, 0)
data, _ := json.Marshal(content)
loginSiteInfo.Content = string(data)
_, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
interfaceSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeInterface,
}
exist, err = x.Context(ctx).Get(interfaceSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
siteUsers := &schema.SiteUsersReq{
AllowUpdateDisplayName: true,
AllowUpdateUsername: true,
AllowUpdateAvatar: true,
AllowUpdateBio: true,
AllowUpdateWebsite: true,
AllowUpdateLocation: true,
}
if exist {
siteUsers.DefaultAvatar = gjson.Get(interfaceSiteInfo.Content, "default_avatar").String()
}
data, _ := json.Marshal(siteUsers)
exist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: constant.SiteTypeUsers})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
usersSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeUsers,
Content: string(data),
Status: 1,
}
_, err = x.Context(ctx).Insert(usersSiteInfo)
if err != nil {
return fmt.Errorf("insert site info failed: %w", err)
}
}
return nil
}
+61
View File
@@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func updateRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error {
defaultConfigTable := []*entity.Config{
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return nil
}
+81
View File
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"time"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
type QuestionPostTime struct {
ID string `xorm:"not null pk BIGINT(20) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 INT(11) status"`
Pin int `xorm:"not null default 1 INT(11) pin"`
Show int `xorm:"not null default 1 INT(11) show"`
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
AnswerCount int `xorm:"not null default 0 INT(11) answer_count"`
CollectionCount int `xorm:"not null default 0 INT(11) collection_count"`
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
func (QuestionPostTime) TableName() string {
return "question"
}
func updateQuestionPostTime(ctx context.Context, x *xorm.Engine) error {
questionList := make([]QuestionPostTime, 0)
err := x.Context(ctx).Find(&questionList, &entity.Question{})
if err != nil {
return fmt.Errorf("get questions failed: %w", err)
}
for _, item := range questionList {
if item.PostUpdateTime.IsZero() {
if !item.UpdatedAt.IsZero() {
item.PostUpdateTime = item.UpdatedAt
} else if !item.CreatedAt.IsZero() {
item.PostUpdateTime = item.CreatedAt
}
if _, err = x.Context(ctx).Update(item, &QuestionPostTime{ID: item.ID}); err != nil {
log.Errorf("update %+v config failed: %s", item, err)
return fmt.Errorf("update question failed: %w", err)
}
}
}
return nil
}
+426
View File
@@ -0,0 +1,426 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"time"
"xorm.io/builder"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func updateCount(ctx context.Context, x *xorm.Engine) error {
fns := []func(ctx context.Context, x *xorm.Engine) error{
inviteAnswer,
addPrivilegeForInviteSomeoneToAnswer,
addGravatarBaseURL,
updateQuestionCount,
updateTagCount,
updateUserQuestionCount,
updateUserAnswerCount,
inBoxData,
}
for _, fn := range fns {
if err := fn(ctx, x); err != nil {
return err
}
}
return nil
}
func addGravatarBaseURL(ctx context.Context, x *xorm.Engine) error {
usersSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeUsers,
}
exist, err := x.Context(ctx).Get(usersSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
content := &schema.SiteUsersReq{}
_ = json.Unmarshal([]byte(usersSiteInfo.Content), content)
content.GravatarBaseURL = "https://www.gravatar.com/avatar/"
data, _ := json.Marshal(content)
usersSiteInfo.Content = string(data)
_, err = x.Context(ctx).ID(usersSiteInfo.ID).Cols("content").Update(usersSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
return nil
}
func addPrivilegeForInviteSomeoneToAnswer(ctx context.Context, x *xorm.Engine) error {
// add rank for invite to answer
powers := []*entity.Power{
{ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"},
}
for _, power := range powers {
exist, err := x.Context(ctx).Get(&entity.Power{PowerType: power.PowerType})
if err != nil {
return err
}
if exist {
_, err = x.Context(ctx).ID(power.ID).Update(power)
} else {
_, err = x.Context(ctx).Insert(power)
}
if err != nil {
return err
}
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer},
{RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer},
}
for _, rel := range rolePowerRels {
exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Context(ctx).Insert(rel)
if err != nil {
return err
}
}
defaultConfigTable := []*entity.Config{
{ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`},
}
for _, c := range defaultConfigTable {
exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil {
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
return fmt.Errorf("add config failed: %w", err)
}
}
return nil
}
func updateQuestionCount(ctx context.Context, x *xorm.Engine) error {
// question answer count
answers := make([]AnswerV13, 0)
err := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable})
if err != nil {
return fmt.Errorf("get answers failed: %w", err)
}
questionAnswerCount := make(map[string]int)
for _, answer := range answers {
_, ok := questionAnswerCount[answer.QuestionID]
if !ok {
questionAnswerCount[answer.QuestionID] = 1
} else {
questionAnswerCount[answer.QuestionID]++
}
}
questionList := make([]QuestionV13, 0)
err = x.Context(ctx).Find(&questionList, &QuestionV13{})
if err != nil {
return fmt.Errorf("get questions failed: %w", err)
}
for _, item := range questionList {
_, ok := questionAnswerCount[item.ID]
if ok {
item.AnswerCount = questionAnswerCount[item.ID]
if _, err = x.Context(ctx).Cols("answer_count").Update(item, &QuestionV13{ID: item.ID}); err != nil {
log.Errorf("update %+v config failed: %s", item, err)
return fmt.Errorf("update question failed: %w", err)
}
}
}
return nil
}
// updateTagCount update tag count
func updateTagCount(ctx context.Context, x *xorm.Engine) error {
tagRelList := make([]entity.TagRel, 0)
err := x.Context(ctx).Find(&tagRelList, &entity.TagRel{})
if err != nil {
return fmt.Errorf("get tag rel failed: %w", err)
}
questionIDs := make([]string, 0)
questionsAvailableMap := make(map[string]bool)
questionsHideMap := make(map[string]bool)
for _, item := range tagRelList {
questionIDs = append(questionIDs, item.ObjectID)
questionsAvailableMap[item.ObjectID] = false
questionsHideMap[item.ObjectID] = false
}
questionList := make([]QuestionV13, 0)
err = x.Context(ctx).In("id", questionIDs).And(builder.Lt{"question.status": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{})
if err != nil {
return fmt.Errorf("get questions failed: %w", err)
}
for _, question := range questionList {
_, ok := questionsAvailableMap[question.ID]
if ok {
questionsAvailableMap[question.ID] = true
if question.Show == entity.QuestionHide {
questionsHideMap[question.ID] = true
}
}
}
for id, ok := range questionsHideMap {
if ok {
if _, err = x.Context(ctx).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}, &entity.TagRel{ObjectID: id}); err != nil {
log.Errorf("update %+v config failed: %s", id, err)
}
}
}
for id, ok := range questionsAvailableMap {
if !ok {
if _, err = x.Context(ctx).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}, &entity.TagRel{ObjectID: id}); err != nil {
log.Errorf("update %+v config failed: %s", id, err)
}
}
}
// select tag count
newTagRelList := make([]entity.TagRel, 0)
err = x.Context(ctx).Find(&newTagRelList, &entity.TagRel{Status: entity.TagRelStatusAvailable})
if err != nil {
return fmt.Errorf("get tag rel failed: %w", err)
}
tagCountMap := make(map[string]int)
for _, v := range newTagRelList {
_, ok := tagCountMap[v.TagID]
if !ok {
tagCountMap[v.TagID] = 1
} else {
tagCountMap[v.TagID]++
}
}
TagList := make([]entity.Tag, 0)
err = x.Context(ctx).Find(&TagList, &entity.Tag{})
if err != nil {
return fmt.Errorf("get tag failed: %w", err)
}
for _, tag := range TagList {
_, ok := tagCountMap[tag.ID]
if ok {
tag.QuestionCount = tagCountMap[tag.ID]
if _, err = x.Context(ctx).Update(tag, &entity.Tag{ID: tag.ID}); err != nil {
log.Errorf("update %+v tag failed: %s", tag.ID, err)
return fmt.Errorf("update tag failed: %w", err)
}
} else {
tag.QuestionCount = 0
if _, err = x.Context(ctx).Cols("question_count").Update(tag, &entity.Tag{ID: tag.ID}); err != nil {
log.Errorf("update %+v tag failed: %s", tag.ID, err)
return fmt.Errorf("update tag failed: %w", err)
}
}
}
return nil
}
// updateUserQuestionCount update user question count
func updateUserQuestionCount(ctx context.Context, x *xorm.Engine) error {
questionList := make([]QuestionV13, 0)
err := x.Context(ctx).Where(builder.Lt{"status": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{})
if err != nil {
return fmt.Errorf("get question failed: %w", err)
}
userQuestionCountMap := make(map[string]int)
for _, question := range questionList {
_, ok := userQuestionCountMap[question.UserID]
if !ok {
userQuestionCountMap[question.UserID] = 1
} else {
userQuestionCountMap[question.UserID]++
}
}
userList := make([]entity.User, 0)
err = x.Context(ctx).Find(&userList, &entity.User{})
if err != nil {
return fmt.Errorf("get user failed: %w", err)
}
for _, user := range userList {
_, ok := userQuestionCountMap[user.ID]
if ok {
user.QuestionCount = userQuestionCountMap[user.ID]
if _, err = x.Context(ctx).Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil {
log.Errorf("update %+v user failed: %s", user.ID, err)
return fmt.Errorf("update user failed: %w", err)
}
} else {
user.QuestionCount = 0
if _, err = x.Context(ctx).Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil {
log.Errorf("update %+v user failed: %s", user.ID, err)
return fmt.Errorf("update user failed: %w", err)
}
}
}
return nil
}
type AnswerV13 struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
Status int `xorm:"not null default 1 INT(11) status"`
Accepted int `xorm:"not null default 1 INT(11) adopted"`
}
func (AnswerV13) TableName() string {
return "answer"
}
// updateUserAnswerCount update user answer count
func updateUserAnswerCount(ctx context.Context, x *xorm.Engine) error {
answers := make([]AnswerV13, 0)
err := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable})
if err != nil {
return fmt.Errorf("get answers failed: %w", err)
}
userAnswerCount := make(map[string]int)
for _, answer := range answers {
_, ok := userAnswerCount[answer.UserID]
if !ok {
userAnswerCount[answer.UserID] = 1
} else {
userAnswerCount[answer.UserID]++
}
}
userList := make([]entity.User, 0)
err = x.Context(ctx).Find(&userList, &entity.User{})
if err != nil {
return fmt.Errorf("get user failed: %w", err)
}
for _, user := range userList {
_, ok := userAnswerCount[user.ID]
if ok {
user.AnswerCount = userAnswerCount[user.ID]
if _, err = x.Context(ctx).Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil {
log.Errorf("update %+v user failed: %s", user.ID, err)
return fmt.Errorf("update user failed: %w", err)
}
} else {
user.AnswerCount = 0
if _, err = x.Context(ctx).Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil {
log.Errorf("update %+v user failed: %s", user.ID, err)
return fmt.Errorf("update user failed: %w", err)
}
}
}
return nil
}
type QuestionV13 struct {
ID string `xorm:"not null pk BIGINT(20) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
InviteUserID string `xorm:"TEXT invite_user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 INT(11) status"`
Pin int `xorm:"not null default 1 INT(11) pin"`
Show int `xorm:"not null default 1 INT(11) show"`
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
AnswerCount int `xorm:"not null default 0 INT(11) answer_count"`
CollectionCount int `xorm:"not null default 0 INT(11) collection_count"`
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
func (QuestionV13) TableName() string {
return "question"
}
func inviteAnswer(ctx context.Context, x *xorm.Engine) error {
err := x.Context(ctx).Sync(new(QuestionV13))
if err != nil {
return err
}
return nil
}
// inBoxData Classify messages
func inBoxData(ctx context.Context, x *xorm.Engine) error {
type Notification struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"`
Content string `xorm:"not null TEXT content"`
Type int `xorm:"not null default 0 INT(11) type"`
MsgType int `xorm:"not null default 0 INT(11) msg_type"`
IsRead int `xorm:"not null default 1 INT(11) is_read"`
Status int `xorm:"not null default 1 INT(11) status"`
}
err := x.Context(ctx).Sync(new(Notification))
if err != nil {
return err
}
msglist := make([]entity.Notification, 0)
err = x.Context(ctx).Find(&msglist, &entity.Notification{})
if err != nil {
return fmt.Errorf("get Notification failed: %w", err)
}
for _, v := range msglist {
Content := &schema.NotificationContent{}
err := json.Unmarshal([]byte(v.Content), Content)
if err != nil {
continue
}
_, ok := constant.NotificationMsgTypeMapping[Content.NotificationAction]
if ok {
v.MsgType = constant.NotificationMsgTypeMapping[Content.NotificationAction]
if _, err = x.Context(ctx).Update(v, &entity.Notification{ID: v.ID}); err != nil {
log.Errorf("update %+v Notification failed: %s", v.ID, err)
}
}
}
return nil
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"time"
"xorm.io/xorm/schemas"
"xorm.io/xorm"
)
func updateTheLengthOfRevisionContent(ctx context.Context, x *xorm.Engine) (err error) {
sess := x.Context(ctx)
if x.Dialect().URI().DBType == schemas.MYSQL {
_, err = sess.Exec("ALTER TABLE `revision` CHANGE `content` `content` MEDIUMTEXT NOT NULL;")
}
return err
}
type RevisionV14 struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
ObjectType int `xorm:"not null default 0 INT(11) object_type"`
ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"`
Title string `xorm:"not null default '' VARCHAR(255) title"`
Content string `xorm:"not null MEDIUMTEXT content"`
Log string `xorm:"VARCHAR(255) log"`
Status int `xorm:"not null default 1 INT(11) status"`
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
}
func (RevisionV14) TableName() string {
return "revision"
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func addNoticeConfig(ctx context.Context, x *xorm.Engine) error {
return x.Context(ctx).Sync(new(entity.UserNotificationConfig))
}
+58
View File
@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func setDefaultUserNotificationConfig(ctx context.Context, x *xorm.Engine) error {
userIDs := make([]string, 0)
err := x.Context(ctx).Table("user").Select("id").Find(&userIDs)
if err != nil {
return err
}
for _, id := range userIDs {
bean := entity.UserNotificationConfig{UserID: id, Source: string(constant.InboxSource)}
exist, err := x.Context(ctx).Get(&bean)
if err != nil {
log.Error(err)
}
if exist {
continue
}
_, err = x.Context(ctx).Insert(&entity.UserNotificationConfig{
UserID: id,
Source: string(constant.InboxSource),
Channels: `[{"key":"email","enable":true}]`,
Enabled: true,
})
if err != nil {
log.Error(err)
}
}
return nil
}
+99
View File
@@ -0,0 +1,99 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addRecoverPermission(ctx context.Context, x *xorm.Engine) error {
powers := []*entity.Power{
{ID: 39, Name: "recover answer", PowerType: permission.AnswerUnDelete, Description: "recover deleted answer"},
{ID: 40, Name: "recover question", PowerType: permission.QuestionUnDelete, Description: "recover deleted question"},
{ID: 41, Name: "recover tag", PowerType: permission.TagUnDelete, Description: "recover deleted tag"},
}
for _, power := range powers {
exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID})
if err != nil {
return err
}
if exist {
_, err = x.Context(ctx).ID(power.ID).Update(power)
} else {
_, err = x.Context(ctx).Insert(power)
}
if err != nil {
return err
}
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AnswerUnDelete},
{RoleID: 2, PowerType: permission.QuestionUnDelete},
{RoleID: 2, PowerType: permission.TagUnDelete},
{RoleID: 3, PowerType: permission.AnswerUnDelete},
{RoleID: 3, PowerType: permission.QuestionUnDelete},
{RoleID: 3, PowerType: permission.TagUnDelete},
}
for _, rel := range rolePowerRels {
exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Context(ctx).Insert(rel)
if err != nil {
return err
}
}
defaultConfigTable := []*entity.Config{
{ID: 128, Key: "rank.answer.undeleted", Value: `-1`},
{ID: 129, Key: "rank.question.undeleted", Value: `-1`},
{ID: 130, Key: "rank.tag.undeleted", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return nil
}
+83
View File
@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"xorm.io/xorm"
)
func addPasswordLoginControl(ctx context.Context, x *xorm.Engine) error {
loginSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeLogin,
}
exist, err := x.Context(ctx).Get(loginSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
content := &schema.SiteLoginReq{}
_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
content.AllowPasswordLogin = true
data, _ := json.Marshal(content)
loginSiteInfo.Content = string(data)
_, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
writeSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeWrite,
}
exist, err = x.Context(ctx).Get(writeSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
type OldSiteWriteReq struct {
RestrictAnswer bool `validate:"omitempty" form:"restrict_answer" json:"restrict_answer"`
RequiredTag bool `validate:"omitempty" form:"required_tag" json:"required_tag"`
RecommendTags []string `validate:"omitempty" form:"recommend_tags" json:"recommend_tags"`
ReservedTags []string `validate:"omitempty" form:"reserved_tags" json:"reserved_tags"`
UserID string `json:"-"`
}
content := &OldSiteWriteReq{}
_ = json.Unmarshal([]byte(writeSiteInfo.Content), content)
content.RestrictAnswer = true
data, _ := json.Marshal(content)
writeSiteInfo.Content = string(data)
_, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
type User struct {
Avatar string `xorm:"not null default '' VARCHAR(1024) avatar"`
}
return x.Context(ctx).Sync(new(User))
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func addNotificationPluginAndThemeConfig(ctx context.Context, x *xorm.Engine) error {
type User struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
ColorScheme string `xorm:"not null default '' VARCHAR(100) color_scheme"`
}
return x.Context(ctx).Sync(new(entity.PluginUserConfig), new(User))
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"xorm.io/xorm"
)
func addTagRecommendedAndReserved(ctx context.Context, x *xorm.Engine) error {
type Tag struct {
ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
Recommend bool `xorm:"not null default false BOOL recommend"`
Reserved bool `xorm:"not null default false BOOL reserved"`
}
return x.Context(ctx).Sync(new(Tag))
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addReview(ctx context.Context, x *xorm.Engine) error {
c := &entity.Config{Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only."}`}
if _, err := x.Context(ctx).Update(c, &entity.Config{Key: "reason.not_clarity"}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
return x.Context(ctx).Sync(new(entity.Review))
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"xorm.io/xorm"
)
func addQuestionHotScore(ctx context.Context, x *xorm.Engine) error {
type Question struct {
HotScore int `xorm:"not null default 0 INT(11) hot_score"`
}
return x.Context(ctx).Sync(new(Question))
}
+79
View File
@@ -0,0 +1,79 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/repo/unique"
"xorm.io/xorm"
)
func addBadges(ctx context.Context, x *xorm.Engine) (err error) {
uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: x})
err = x.Context(ctx).Sync(new(entity.Badge), new(entity.BadgeGroup), new(entity.BadgeAward))
if err != nil {
return fmt.Errorf("sync table failed: %w", err)
}
for _, badgeGroup := range defaultBadgeGroupTable {
exist, err := x.Context(ctx).Get(&entity.BadgeGroup{ID: badgeGroup.ID})
if err != nil {
return err
}
if exist {
_, err = x.Context(ctx).ID(badgeGroup.ID).Update(badgeGroup)
} else {
_, err = x.Context(ctx).Insert(badgeGroup)
}
if err != nil {
return fmt.Errorf("insert badge group failed: %w", err)
}
}
for _, badge := range defaultBadgeTable {
beans := &entity.Badge{Name: badge.Name}
exist, err := x.Context(ctx).Get(beans)
if err != nil {
return err
}
if exist {
badge.ID = beans.ID
_, err = x.Context(ctx).ID(beans.ID).Update(badge)
if err != nil {
return fmt.Errorf("update badge failed: %w", err)
}
continue
}
badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, new(entity.Badge).TableName())
if err != nil {
return err
}
if _, err := x.Context(ctx).Insert(badge); err != nil {
return err
}
}
return
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func addQuestionLink(ctx context.Context, x *xorm.Engine) (err error) {
return x.Context(ctx).Sync(new(entity.QuestionLink))
}
+70
View File
@@ -0,0 +1,70 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"xorm.io/xorm"
)
func addQuestionLinkedCount(ctx context.Context, x *xorm.Engine) error {
writeSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeWrite,
}
exist, err := x.Context(ctx).Get(writeSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
type OldSiteWriteReq struct {
RestrictAnswer bool `json:"restrict_answer"`
RequiredTag bool `json:"required_tag"`
RecommendTags []*schema.SiteWriteTag `json:"recommend_tags"`
ReservedTags []*schema.SiteWriteTag `json:"reserved_tags"`
MaxImageSize int `json:"max_image_size"`
MaxAttachmentSize int `json:"max_attachment_size"`
MaxImageMegapixel int `json:"max_image_megapixel"`
AuthorizedImageExtensions []string `json:"authorized_image_extensions"`
AuthorizedAttachmentExtensions []string `json:"authorized_attachment_extensions"`
}
content := &OldSiteWriteReq{}
_ = json.Unmarshal([]byte(writeSiteInfo.Content), content)
content.MaxImageSize = 4
content.MaxAttachmentSize = 8
content.MaxImageMegapixel = 40
content.AuthorizedImageExtensions = []string{"jpg", "jpeg", "png", "gif", "webp"}
content.AuthorizedAttachmentExtensions = []string{}
data, _ := json.Marshal(content)
writeSiteInfo.Content = string(data)
_, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
return x.Context(ctx).Sync(new(entity.Question))
}
+65
View File
@@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func addFileRecord(ctx context.Context, x *xorm.Engine) error {
if err := x.Context(ctx).Sync(new(entity.FileRecord)); err != nil {
return err
}
// Set default external_content_display to always_display
legalInfo := &entity.SiteInfo{Type: "legal"}
exist, err := x.Context(ctx).Get(legalInfo)
if err != nil {
return fmt.Errorf("get legal config failed: %w", err)
}
legalConfig := make(map[string]any)
if exist {
if err := json.Unmarshal([]byte(legalInfo.Content), &legalConfig); err != nil {
return fmt.Errorf("unmarshal legal config failed: %w", err)
}
}
legalConfig["external_content_display"] = "always_display"
legalConfigBytes, _ := json.Marshal(legalConfig)
if exist {
legalInfo.Content = string(legalConfigBytes)
_, err = x.Context(ctx).ID(legalInfo.ID).Cols("content").Update(legalInfo)
if err != nil {
return fmt.Errorf("update legal config failed: %w", err)
}
} else {
legalInfo.Content = string(legalConfigBytes)
legalInfo.Status = 1
_, err = x.Context(ctx).Insert(legalInfo)
if err != nil {
return fmt.Errorf("insert legal config failed: %w", err)
}
}
return nil
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func addPluginKVStorage(ctx context.Context, x *xorm.Engine) error {
return x.Context(ctx).Sync(new(entity.PluginKVStorage))
}
+83
View File
@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"xorm.io/xorm"
)
func addSuspendedUntilToUser(ctx context.Context, x *xorm.Engine) error {
type User struct {
SuspendedUntil *time.Time `xorm:"DATETIME suspended_until"`
}
return x.Context(ctx).Sync(new(User))
}
func moveUserConfigToInterface(ctx context.Context, x *xorm.Engine) error {
if err := addSuspendedUntilToUser(ctx, x); err != nil {
return fmt.Errorf("add suspended_until to user failed: %w", err)
}
// Get old interface config
interfaceSiteInfo := &entity.SiteInfo{Type: constant.SiteTypeInterface}
exist, err := x.Context(ctx).Get(interfaceSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
return fmt.Errorf("interface site info not found")
}
interfaceConfig := &schema.SiteInterfaceReq{}
_ = json.Unmarshal([]byte(interfaceSiteInfo.Content), interfaceConfig)
// Get old user config
usersConfig := &entity.SiteInfo{Type: constant.SiteTypeUsers}
exist, err = x.Context(ctx).Get(usersConfig)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
return fmt.Errorf("users site info not found")
}
siteUsers := &schema.SiteUsersReq{}
_ = json.Unmarshal([]byte(usersConfig.Content), siteUsers)
interfaceConfig.DefaultAvatar = siteUsers.DefaultAvatar
interfaceConfig.GravatarBaseURL = siteUsers.GravatarBaseURL
interfaceConfigByte, _ := json.Marshal(interfaceConfig)
interfaceSiteInfo.Content = string(interfaceConfigByte)
_, err = x.Context(ctx).ID(interfaceSiteInfo.ID).Update(interfaceSiteInfo)
if err != nil {
return fmt.Errorf("insert site info failed: %w", err)
}
return nil
}
+69
View File
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"xorm.io/xorm"
)
func addOptionalTags(ctx context.Context, x *xorm.Engine) error {
writeSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeWrite,
}
exist, err := x.Context(ctx).Get(writeSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
type OldSiteWriteReq struct {
MinimumContent int `json:"min_content"`
RestrictAnswer bool `json:"restrict_answer"`
MinimumTags int `json:"min_tags"`
RequiredTag bool `json:"required_tag"`
RecommendTags []*schema.SiteWriteTag `json:"recommend_tags"`
ReservedTags []*schema.SiteWriteTag `json:"reserved_tags"`
MaxImageSize int `json:"max_image_size"`
MaxAttachmentSize int `json:"max_attachment_size"`
MaxImageMegapixel int `json:"max_image_megapixel"`
AuthorizedImageExtensions []string `json:"authorized_image_extensions"`
AuthorizedAttachmentExtensions []string `json:"authorized_attachment_extensions"`
}
content := &OldSiteWriteReq{}
_ = json.Unmarshal([]byte(writeSiteInfo.Content), content)
content.MinimumTags = 1
content.MinimumContent = 6
data, _ := json.Marshal(content)
writeSiteInfo.Content = string(data)
_, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
return nil
}
+37
View File
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"xorm.io/xorm"
)
func expandAvatarColumnLength(ctx context.Context, x *xorm.Engine) error {
type User struct {
Avatar string `xorm:"not null default '' VARCHAR(2048) avatar"`
}
if err := x.Context(ctx).Sync(new(User)); err != nil {
return fmt.Errorf("expand avatar column length failed: %w", err)
}
return nil
}
+233
View File
@@ -0,0 +1,233 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"time"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
func addActivityTimeline(ctx context.Context, x *xorm.Engine) (err error) {
switch x.Dialect().URI().DBType {
case schemas.MYSQL:
_, err = x.Context(ctx).Exec("ALTER TABLE `answer` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL")
if err != nil {
return err
}
_, err = x.Context(ctx).Exec("ALTER TABLE `question` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL")
if err != nil {
return err
}
case schemas.POSTGRES:
_, err = x.Context(ctx).Exec(`ALTER TABLE "answer" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`)
if err != nil {
return err
}
_, err = x.Context(ctx).Exec(`ALTER TABLE "question" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`)
if err != nil {
return err
}
case schemas.SQLITE:
_, err = x.Context(ctx).Exec(`DROP INDEX "IDX_answer_user_id";
ALTER TABLE "answer" RENAME TO "_answer_old_v3";
CREATE TABLE "answer" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME DEFAULT NULL,
"question_id" INTEGER NOT NULL DEFAULT 0,
"user_id" INTEGER NOT NULL DEFAULT 0,
"original_text" TEXT NOT NULL,
"parsed_text" TEXT NOT NULL,
"status" INTEGER NOT NULL DEFAULT 1,
"adopted" INTEGER NOT NULL DEFAULT 1,
"comment_count" INTEGER NOT NULL DEFAULT 0,
"vote_count" INTEGER NOT NULL DEFAULT 0,
"revision_id" INTEGER NOT NULL DEFAULT 0
);
INSERT INTO "answer" ("id", "created_at", "updated_at", "question_id", "user_id", "original_text", "parsed_text", "status", "adopted", "comment_count", "vote_count", "revision_id") SELECT "id", "created_at", "updated_at", "question_id", "user_id", "original_text", "parsed_text", "status", "adopted", "comment_count", "vote_count", "revision_id" FROM "_answer_old_v3";
CREATE INDEX "IDX_answer_user_id"
ON "answer" (
"user_id" ASC
);
DROP INDEX "IDX_question_user_id";
ALTER TABLE "question" RENAME TO "_question_old_v3";
CREATE TABLE "question" (
"id" INTEGER NOT NULL,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME DEFAULT NULL,
"user_id" INTEGER NOT NULL DEFAULT 0,
"title" TEXT NOT NULL DEFAULT '',
"original_text" TEXT NOT NULL,
"parsed_text" TEXT NOT NULL,
"status" INTEGER NOT NULL DEFAULT 1,
"view_count" INTEGER NOT NULL DEFAULT 0,
"unique_view_count" INTEGER NOT NULL DEFAULT 0,
"vote_count" INTEGER NOT NULL DEFAULT 0,
"answer_count" INTEGER NOT NULL DEFAULT 0,
"collection_count" INTEGER NOT NULL DEFAULT 0,
"follow_count" INTEGER NOT NULL DEFAULT 0,
"accepted_answer_id" INTEGER NOT NULL DEFAULT 0,
"last_answer_id" INTEGER NOT NULL DEFAULT 0,
"post_update_time" DATETIME DEFAULT CURRENT_TIMESTAMP,
"revision_id" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
INSERT INTO "question" ("id", "created_at", "updated_at", "user_id", "title", "original_text", "parsed_text", "status", "view_count", "unique_view_count", "vote_count", "answer_count", "collection_count", "follow_count", "accepted_answer_id", "last_answer_id", "post_update_time", "revision_id") SELECT "id", "created_at", "updated_at", "user_id", "title", "original_text", "parsed_text", "status", "view_count", "unique_view_count", "vote_count", "answer_count", "collection_count", "follow_count", "accepted_answer_id", "last_answer_id", "post_update_time", "revision_id" FROM "_question_old_v3";
CREATE INDEX "IDX_question_user_id"
ON "question" (
"user_id" ASC
);`)
if err != nil {
return err
}
}
// only increasing field length to 128
type Config struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
Key string `xorm:"unique VARCHAR(128) key"`
}
if err := x.Context(ctx).Sync(new(Config)); err != nil {
return fmt.Errorf("sync config table failed: %w", err)
}
defaultConfigTable := []*entity.Config{
{ID: 36, Key: "rank.question.add", Value: `1`},
{ID: 37, Key: "rank.question.edit", Value: `200`},
{ID: 38, Key: "rank.question.delete", Value: `-1`},
{ID: 39, Key: "rank.question.vote_up", Value: `15`},
{ID: 40, Key: "rank.question.vote_down", Value: `125`},
{ID: 41, Key: "rank.answer.add", Value: `1`},
{ID: 42, Key: "rank.answer.edit", Value: `200`},
{ID: 43, Key: "rank.answer.delete", Value: `-1`},
{ID: 44, Key: "rank.answer.accept", Value: `-1`},
{ID: 45, Key: "rank.answer.vote_up", Value: `15`},
{ID: 46, Key: "rank.answer.vote_down", Value: `125`},
{ID: 47, Key: "rank.comment.add", Value: `1`},
{ID: 48, Key: "rank.comment.edit", Value: `-1`},
{ID: 49, Key: "rank.comment.delete", Value: `-1`},
{ID: 50, Key: "rank.report.add", Value: `1`},
{ID: 51, Key: "rank.tag.add", Value: `1500`},
{ID: 52, Key: "rank.tag.edit", Value: `100`},
{ID: 53, Key: "rank.tag.delete", Value: `-1`},
{ID: 54, Key: "rank.tag.synonym", Value: `20000`},
{ID: 55, Key: "rank.link.url_limit", Value: `10`},
{ID: 56, Key: "rank.vote.detail", Value: `0`},
{ID: 87, Key: "question.asked", Value: `0`},
{ID: 88, Key: "question.closed", Value: `0`},
{ID: 89, Key: "question.reopened", Value: `0`},
{ID: 90, Key: "question.answered", Value: `0`},
{ID: 91, Key: "question.commented", Value: `0`},
{ID: 92, Key: "question.accept", Value: `0`},
{ID: 93, Key: "question.edited", Value: `0`},
{ID: 94, Key: "question.rollback", Value: `0`},
{ID: 95, Key: "question.deleted", Value: `0`},
{ID: 96, Key: "question.undeleted", Value: `0`},
{ID: 97, Key: "answer.answered", Value: `0`},
{ID: 98, Key: "answer.commented", Value: `0`},
{ID: 99, Key: "answer.edited", Value: `0`},
{ID: 100, Key: "answer.rollback", Value: `0`},
{ID: 101, Key: "answer.undeleted", Value: `0`},
{ID: 102, Key: "tag.created", Value: `0`},
{ID: 103, Key: "tag.edited", Value: `0`},
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
{ID: 107, Key: "rank.comment.vote_up", Value: `1`},
{ID: 108, Key: "rank.comment.vote_down", Value: `1`},
{ID: 109, Key: "rank.question.edit_without_review", Value: `2000`},
{ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`},
{ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`},
{ID: 112, Key: "rank.answer.audit", Value: `2000`},
{ID: 113, Key: "rank.question.audit", Value: `2000`},
{ID: 114, Key: "rank.tag.audit", Value: `20000`},
}
for _, c := range defaultConfigTable {
exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
type Revision struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"`
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
}
type Activity struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"`
UserID string `xorm:"not null index BIGINT(20) user_id"`
TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"`
ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"`
RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"`
OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"`
}
type Tag struct {
ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
}
type Question struct {
ID string `xorm:"not null pk BIGINT(20) id"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
}
type Answer struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
}
err = x.Context(ctx).Sync(new(Activity), new(Revision), new(Tag), new(Question), new(Answer))
if err != nil {
return fmt.Errorf("sync table failed %w", err)
}
return nil
}
+397
View File
@@ -0,0 +1,397 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
"xorm.io/xorm"
)
func updateAdminMenuSettings(ctx context.Context, x *xorm.Engine) (err error) {
err = splitWriteMenu(ctx, x)
if err != nil {
return
}
err = splitInterfaceMenu(ctx, x)
if err != nil {
return
}
err = splitLegalMenu(ctx, x)
if err != nil {
return
}
return
}
// splitWriteMenu splits the site write settings into advanced, questions, and tags settings
func splitWriteMenu(ctx context.Context, x *xorm.Engine) error {
var (
siteInfo = &entity.SiteInfo{}
siteInfoAdvanced = &entity.SiteInfo{}
siteInfoQuestions = &entity.SiteInfo{}
siteInfoTags = &entity.SiteInfo{}
)
exist, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeWrite}).Get(siteInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return err
}
if !exist {
return nil
}
siteWrite := &schema.SiteWriteResp{}
if err := json.Unmarshal([]byte(siteInfo.Content), siteWrite); err != nil {
return err
}
// site advanced settings
siteAdvanced := &schema.SiteAdvancedResp{
MaxImageSize: siteWrite.MaxImageSize,
MaxAttachmentSize: siteWrite.MaxAttachmentSize,
MaxImageMegapixel: siteWrite.MaxImageMegapixel,
AuthorizedImageExtensions: siteWrite.AuthorizedImageExtensions,
AuthorizedAttachmentExtensions: siteWrite.AuthorizedAttachmentExtensions,
}
// site questions settings
siteQuestions := &schema.SiteQuestionsResp{
MinimumTags: siteWrite.MinimumTags,
MinimumContent: siteWrite.MinimumContent,
RestrictAnswer: siteWrite.RestrictAnswer,
}
// site tags settings
siteTags := &schema.SiteTagsResp{
ReservedTags: siteWrite.ReservedTags,
RecommendTags: siteWrite.RecommendTags,
RequiredTag: siteWrite.RequiredTag,
}
// save site settings
// save advanced settings
existsAdvanced, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeWrite}).Get(siteInfoAdvanced)
if err != nil {
return err
}
advancedContent, err := json.Marshal(siteAdvanced)
if err != nil {
return err
}
if !existsAdvanced {
_, err = x.Context(ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypeAdvanced,
Content: string(advancedContent),
Status: 1,
})
if err != nil {
return err
}
}
// save questions settings
existsQuestions, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeQuestions}).Get(siteInfoQuestions)
if err != nil {
return err
}
questionsContent, err := json.Marshal(siteQuestions)
if err != nil {
return err
}
if !existsQuestions {
_, err = x.Context(ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypeQuestions,
Content: string(questionsContent),
Status: 1,
})
if err != nil {
return err
}
}
// save tags settings
existsTags, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeTags}).Get(siteInfoTags)
if err != nil {
return err
}
tagsContent, err := json.Marshal(siteTags)
if err != nil {
return err
}
if !existsTags {
_, err = x.Context(ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypeTags,
Content: string(tagsContent),
Status: 1,
})
if err != nil {
return err
}
}
return nil
}
// splitInterfaceMenu splits the site interface settings into interface and user settings
func splitInterfaceMenu(ctx context.Context, x *xorm.Engine) error {
var (
siteInfo = &entity.SiteInfo{}
siteInfoInterface = &entity.SiteInfo{}
siteInfoUsers = &entity.SiteInfo{}
)
type SiteInterface struct {
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
DefaultAvatar string `validate:"required,oneof=system gravatar" json:"default_avatar"`
GravatarBaseURL string `validate:"omitempty" json:"gravatar_base_url"`
}
exist, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeInterface}).Get(siteInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return err
}
if !exist {
return nil
}
oldSiteInterface := &SiteInterface{}
if err := json.Unmarshal([]byte(siteInfo.Content), oldSiteInterface); err != nil {
return err
}
siteUser := &schema.SiteUsersSettingsResp{
DefaultAvatar: oldSiteInterface.DefaultAvatar,
GravatarBaseURL: oldSiteInterface.GravatarBaseURL,
}
siteInterface := &schema.SiteInterfaceResp{
Language: oldSiteInterface.Language,
TimeZone: oldSiteInterface.TimeZone,
}
// save settings
// save user settings
existsUsers, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeUsersSettings}).Get(siteInfoUsers)
if err != nil {
return err
}
userContent, err := json.Marshal(siteUser)
if err != nil {
return err
}
if !existsUsers {
_, err = x.Context(ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypeUsersSettings,
Content: string(userContent),
Status: 1,
})
if err != nil {
return err
}
}
// save interface settings
existsInterface, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeInterfaceSettings}).Get(siteInfoInterface)
if err != nil {
return err
}
interfaceContent, err := json.Marshal(siteInterface)
if err != nil {
return err
}
if !existsInterface {
_, err = x.Context(ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypeInterfaceSettings,
Content: string(interfaceContent),
Status: 1,
})
if err != nil {
return err
}
}
return nil
}
// splitLegalMenu splits the site legal settings into policies and security settings
func splitLegalMenu(ctx context.Context, x *xorm.Engine) error {
var (
siteInfo = &entity.SiteInfo{}
siteInfoPolices = &entity.SiteInfo{}
siteInfoSecurity = &entity.SiteInfo{}
siteInfoLogin = &entity.SiteInfo{}
siteInfoGeneral = &entity.SiteInfo{}
)
type SiteLogin struct {
AllowNewRegistrations bool `json:"allow_new_registrations"`
AllowEmailRegistrations bool `json:"allow_email_registrations"`
AllowPasswordLogin bool `json:"allow_password_login"`
LoginRequired bool `json:"login_required"`
AllowEmailDomains []string `json:"allow_email_domains"`
}
type SiteGeneral struct {
Name string `validate:"required,sanitizer,gt=1,lte=128" form:"name" json:"name"`
ShortDescription string `validate:"omitempty,sanitizer,gt=3,lte=255" form:"short_description" json:"short_description"`
Description string `validate:"omitempty,sanitizer,gt=3,lte=2000" form:"description" json:"description"`
SiteUrl string `validate:"required,sanitizer,gt=1,lte=512,url" form:"site_url" json:"site_url"`
ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
CheckUpdate bool `validate:"omitempty,sanitizer" form:"check_update" json:"check_update"`
}
// find old site legal settings
exist, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeLegal}).Get(siteInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return err
}
if !exist {
return nil
}
oldSiteLegal := &schema.SiteLegalResp{}
if err := json.Unmarshal([]byte(siteInfo.Content), oldSiteLegal); err != nil {
return err
}
// find old site login settings
existsLogin, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeLogin}).Get(siteInfoLogin)
if err != nil {
return err
}
oldSiteLogin := &SiteLogin{}
if err := json.Unmarshal([]byte(siteInfoLogin.Content), oldSiteLogin); err != nil {
return err
}
// find old site general settings
existGeneral, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeGeneral}).Get(siteInfoGeneral)
if err != nil {
return err
}
oldSiteGeneral := &SiteGeneral{}
if err := json.Unmarshal([]byte(siteInfoGeneral.Content), oldSiteGeneral); err != nil {
return err
}
sitePolicies := &schema.SitePoliciesResp{
TermsOfServiceOriginalText: oldSiteLegal.TermsOfServiceOriginalText,
TermsOfServiceParsedText: oldSiteLegal.TermsOfServiceParsedText,
PrivacyPolicyOriginalText: oldSiteLegal.PrivacyPolicyOriginalText,
PrivacyPolicyParsedText: oldSiteLegal.PrivacyPolicyParsedText,
}
siteLogin := &schema.SiteLoginResp{
AllowNewRegistrations: oldSiteLogin.AllowNewRegistrations,
AllowEmailRegistrations: oldSiteLogin.AllowEmailRegistrations,
AllowPasswordLogin: oldSiteLogin.AllowPasswordLogin,
AllowEmailDomains: oldSiteLogin.AllowEmailDomains,
RequireEmailVerification: true,
}
siteGeneral := &schema.SiteGeneralReq{
Name: oldSiteGeneral.Name,
ShortDescription: oldSiteGeneral.ShortDescription,
Description: oldSiteGeneral.Description,
SiteUrl: oldSiteGeneral.SiteUrl,
ContactEmail: oldSiteGeneral.ContactEmail,
}
siteSecurity := &schema.SiteSecurityResp{
LoginRequired: oldSiteLogin.LoginRequired,
ExternalContentDisplay: oldSiteLegal.ExternalContentDisplay,
CheckUpdate: oldSiteGeneral.CheckUpdate,
}
if !existsLogin {
siteSecurity.LoginRequired = false
}
if !existGeneral {
siteSecurity.CheckUpdate = true
}
// save settings
// save policies settings
existsPolicies, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypePolicies}).Get(siteInfoPolices)
if err != nil {
return err
}
policiesContent, err := json.Marshal(sitePolicies)
if err != nil {
return err
}
if !existsPolicies {
_, err = x.Context(ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypePolicies,
Content: string(policiesContent),
Status: 1,
})
if err != nil {
return err
}
}
// save security settings
existsSecurity, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeSecurity}).Get(siteInfoSecurity)
if err != nil {
return err
}
securityContent, err := json.Marshal(siteSecurity)
if err != nil {
return err
}
if !existsSecurity {
_, err = x.Context(ctx).Insert(&entity.SiteInfo{
Type: constant.SiteTypeSecurity,
Content: string(securityContent),
Status: 1,
})
if err != nil {
return err
}
}
// save login settings
if existsLogin {
loginContent, _ := json.Marshal(siteLogin)
_, err = x.Context(ctx).ID(siteInfoLogin.ID).Update(&entity.SiteInfo{
Type: constant.SiteTypeLogin,
Content: string(loginContent),
Status: 1,
})
if err != nil {
return err
}
}
// save general settings
if existGeneral {
generalContent, _ := json.Marshal(siteGeneral)
_, err = x.Context(ctx).ID(siteInfoGeneral.ID).Update(&entity.SiteInfo{
Type: constant.SiteTypeGeneral,
Content: string(generalContent),
Status: 1,
})
if err != nil {
return err
}
}
return nil
}
+116
View File
@@ -0,0 +1,116 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func aiFeat(ctx context.Context, x *xorm.Engine) error {
if err := addAIConversationTables(ctx, x); err != nil {
return fmt.Errorf("add ai conversation tables failed: %w", err)
}
if err := addAPIKey(ctx, x); err != nil {
return fmt.Errorf("add api key failed: %w", err)
}
log.Info("AI feature migration completed successfully")
return nil
}
func addAIConversationTables(ctx context.Context, x *xorm.Engine) error {
if err := x.Context(ctx).Sync(new(entity.AIConversation)); err != nil {
return fmt.Errorf("sync ai_conversation table failed: %w", err)
}
if err := x.Context(ctx).Sync(new(entity.AIConversationRecord)); err != nil {
return fmt.Errorf("sync ai_conversation_record table failed: %w", err)
}
return nil
}
func addAPIKey(ctx context.Context, x *xorm.Engine) error {
err := x.Context(ctx).Sync(new(entity.APIKey))
if err != nil {
return err
}
defaultConfigTable := []*entity.Config{
{ID: 131, Key: "ai_config.provider", Value: `[{"default_api_host":"https://api.openai.com","display_name":"OpenAI","name":"openai"},{"default_api_host":"https://generativelanguage.googleapis.com","display_name":"Gemini","name":"gemini"},{"default_api_host":"https://api.anthropic.com","display_name":"Anthropic","name":"anthropic"}]`},
}
for _, c := range defaultConfigTable {
exist, err := x.Context(ctx).Get(&entity.Config{Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
continue
}
if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
aiSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeAI,
}
exist, err := x.Context(ctx).Get(aiSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
content := &schema.SiteAIReq{}
_ = json.Unmarshal([]byte(aiSiteInfo.Content), content)
content.PromptConfig = &schema.AIPromptConfig{
ZhCN: constant.DefaultAIPromptConfigZhCN,
EnUS: constant.DefaultAIPromptConfigEnUS,
}
data, _ := json.Marshal(content)
aiSiteInfo.Content = string(data)
_, err = x.Context(ctx).ID(aiSiteInfo.ID).Cols("content").Update(aiSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
} else {
content := &schema.SiteAIReq{
PromptConfig: &schema.AIPromptConfig{
ZhCN: constant.DefaultAIPromptConfigZhCN,
EnUS: constant.DefaultAIPromptConfigEnUS,
},
}
data, _ := json.Marshal(content)
aiSiteInfo.Content = string(data)
aiSiteInfo.Type = constant.SiteTypeAI
if _, err = x.Context(ctx).Insert(aiSiteInfo); err != nil {
return fmt.Errorf("insert site info failed: %w", err)
}
log.Infof("insert site info %+v", aiSiteInfo)
}
return nil
}
+37
View File
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func updateAvatarType(ctx context.Context, x *xorm.Engine) error {
// Sync the User struct to the database.
// Since you changed the struct to use TEXT, this will update the column type.
if err := x.Context(ctx).Sync(new(entity.User)); err != nil {
return fmt.Errorf("sync user table failed: %w", err)
}
return nil
}
+39
View File
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
// addAIConversationReasoningContent adds a reasoning_content column to the
// ai_conversation_record table so that the chain-of-thought returned by
// reasoning/thinking-capable models (e.g. DeepSeek) is persisted along with
// the regular content and can be re-displayed when reloading a conversation.
func addAIConversationReasoningContent(ctx context.Context, x *xorm.Engine) error {
if err := x.Context(ctx).Sync(new(entity.AIConversationRecord)); err != nil {
return fmt.Errorf("sync ai_conversation_record table failed: %w", err)
}
return nil
}
+87
View File
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func addRequireEmailVerification(ctx context.Context, x *xorm.Engine) error {
loginSiteInfo := &entity.SiteInfo{}
exist, err := x.Context(ctx).Where("type = ?", constant.SiteTypeLogin).Get(loginSiteInfo)
if err != nil {
return fmt.Errorf("get login config failed: %w", err)
}
if !exist {
return nil
}
content, err := backfillRequireEmailVerification(loginSiteInfo.Content)
if err != nil {
return fmt.Errorf("backfill login config failed: %w", err)
}
loginSiteInfo.Content = content
_, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
if err != nil {
return fmt.Errorf("update login config failed: %w", err)
}
return nil
}
func backfillRequireEmailVerification(content string) (string, error) {
if strings.TrimSpace(content) == "" {
content = "{}"
}
loginConfig := map[string]json.RawMessage{}
if err := json.Unmarshal([]byte(content), &loginConfig); err != nil {
return "", err
}
if loginConfig == nil {
loginConfig = map[string]json.RawMessage{}
}
requireEmailVerification, exists := loginConfig["require_email_verification"]
// Legacy configs that predate this setting should keep the safer behavior.
// Treat a missing or null value as requiring email verification.
if !exists || bytes.Equal(bytes.TrimSpace(requireEmailVerification), []byte("null")) {
loginConfig["require_email_verification"] = json.RawMessage("true")
} else {
var value bool
if err := json.Unmarshal(requireEmailVerification, &value); err != nil {
return "", err
}
loginConfig["require_email_verification"] = requireEmailVerification
}
data, err := json.Marshal(loginConfig)
if err != nil {
return "", err
}
return string(data), nil
}
+172
View File
@@ -0,0 +1,172 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"testing"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
)
func TestBackfillRequireEmailVerification(t *testing.T) {
tests := []struct {
name string
content string
expected bool
}{
{
name: "adds true for missing key",
content: `{"allow_new_registrations":true,"allow_email_registrations":true,"allow_password_login":true}`,
expected: true,
},
{
name: "converts null to true",
content: `{"allow_new_registrations":true,"allow_email_registrations":true,"allow_password_login":true,"require_email_verification":null}`,
expected: true,
},
{
name: "preserves false",
content: `{"allow_new_registrations":true,"allow_email_registrations":true,"allow_password_login":true,"require_email_verification":false}`,
expected: false,
},
{
name: "preserves true",
content: `{"allow_new_registrations":true,"allow_email_registrations":true,"allow_password_login":true,"require_email_verification":true}`,
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
content, err := backfillRequireEmailVerification(tt.content)
require.NoError(t, err)
var result map[string]bool
require.NoError(t, json.Unmarshal([]byte(content), &result))
assert.Equal(t, tt.expected, result["require_email_verification"])
})
}
}
func TestAddRequireEmailVerificationAbsentLoginRow(t *testing.T) {
x, err := xorm.NewEngine("sqlite", ":memory:")
require.NoError(t, err)
defer func() {
_ = x.Close()
}()
require.NoError(t, x.Sync(new(entity.SiteInfo)))
require.NoError(t, addRequireEmailVerification(context.TODO(), x))
}
func TestAddRequireEmailVerificationUpdatesLoginRow(t *testing.T) {
x, err := xorm.NewEngine("sqlite", ":memory:")
require.NoError(t, err)
defer func() {
_ = x.Close()
}()
require.NoError(t, x.Sync(new(entity.SiteInfo)))
_, err = x.Insert(&entity.SiteInfo{
Type: constant.SiteTypeLogin,
Content: `{"allow_new_registrations":true}`,
Status: 1,
})
require.NoError(t, err)
require.NoError(t, addRequireEmailVerification(context.TODO(), x))
login := &entity.SiteInfo{}
exist, err := x.Where("type = ?", constant.SiteTypeLogin).Get(login)
require.NoError(t, err)
require.True(t, exist)
var result struct {
RequireEmailVerification bool `json:"require_email_verification"`
}
require.NoError(t, json.Unmarshal([]byte(login.Content), &result))
assert.True(t, result.RequireEmailVerification)
}
func TestSplitLegalMenuKeepsRequireEmailVerificationDefaultTrue(t *testing.T) {
x, err := xorm.NewEngine("sqlite", ":memory:")
require.NoError(t, err)
defer func() {
_ = x.Close()
}()
require.NoError(t, x.Sync(new(entity.SiteInfo)))
_, err = x.Insert(&entity.SiteInfo{
Type: constant.SiteTypeLegal,
Content: `{
"terms_of_service_original_text":"tos",
"terms_of_service_parsed_text":"tos",
"privacy_policy_original_text":"privacy",
"privacy_policy_parsed_text":"privacy",
"external_content_display":"always_display"
}`,
Status: 1,
})
require.NoError(t, err)
_, err = x.Insert(&entity.SiteInfo{
Type: constant.SiteTypeLogin,
Content: `{
"allow_new_registrations":true,
"allow_email_registrations":true,
"allow_password_login":true,
"login_required":false,
"allow_email_domains":[]
}`,
Status: 1,
})
require.NoError(t, err)
_, err = x.Insert(&entity.SiteInfo{
Type: constant.SiteTypeGeneral,
Content: `{
"name":"site",
"short_description":"short",
"description":"description",
"site_url":"https://example.com",
"contact_email":"admin@example.com",
"check_update":true
}`,
Status: 1,
})
require.NoError(t, err)
require.NoError(t, splitLegalMenu(context.TODO(), x))
login := &entity.SiteInfo{}
exist, err := x.Where("type = ?", constant.SiteTypeLogin).Get(login)
require.NoError(t, err)
require.True(t, exist)
var result struct {
RequireEmailVerification bool `json:"require_email_verification"`
}
require.NoError(t, json.Unmarshal([]byte(login.Content), &result))
assert.True(t, result.RequireEmailVerification)
}
+235
View File
@@ -0,0 +1,235 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addRoleFeatures(ctx context.Context, x *xorm.Engine) error {
err := x.Context(ctx).Sync(new(entity.Role), new(entity.RolePowerRel), new(entity.Power), new(entity.UserRoleRel))
if err != nil {
return err
}
roles := []*entity.Role{
{ID: 1, Name: "User", Description: "Default with no special access."},
{ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
{ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
}
// insert default roles
for _, role := range roles {
exist, err := x.Context(ctx).Get(&entity.Role{ID: role.ID, Name: role.Name})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Context(ctx).Insert(role)
if err != nil {
return err
}
}
powers := []*entity.Power{
{ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
{ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
{ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
{ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
{ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
{ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
{ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
{ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
{ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
{ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
{ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
{ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
{ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
{ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
{ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
{ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
{ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
{ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
{ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
{ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
{ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
{ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
{ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
{ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
{ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
{ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
{ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
{ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
{ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
{ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
{ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
{ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
{ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
}
// insert default powers
for _, power := range powers {
exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID})
if err != nil {
return err
}
if exist {
_, err = x.Context(ctx).ID(power.ID).Update(power)
} else {
_, err = x.Context(ctx).Insert(power)
}
if err != nil {
return err
}
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AdminAccess},
{RoleID: 2, PowerType: permission.QuestionAdd},
{RoleID: 2, PowerType: permission.QuestionEdit},
{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 2, PowerType: permission.QuestionDelete},
{RoleID: 2, PowerType: permission.QuestionClose},
{RoleID: 2, PowerType: permission.QuestionReopen},
{RoleID: 2, PowerType: permission.QuestionVoteUp},
{RoleID: 2, PowerType: permission.QuestionVoteDown},
{RoleID: 2, PowerType: permission.AnswerAdd},
{RoleID: 2, PowerType: permission.AnswerEdit},
{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 2, PowerType: permission.AnswerDelete},
{RoleID: 2, PowerType: permission.AnswerAccept},
{RoleID: 2, PowerType: permission.AnswerVoteUp},
{RoleID: 2, PowerType: permission.AnswerVoteDown},
{RoleID: 2, PowerType: permission.CommentAdd},
{RoleID: 2, PowerType: permission.CommentEdit},
{RoleID: 2, PowerType: permission.CommentDelete},
{RoleID: 2, PowerType: permission.CommentVoteUp},
{RoleID: 2, PowerType: permission.CommentVoteDown},
{RoleID: 2, PowerType: permission.ReportAdd},
{RoleID: 2, PowerType: permission.TagAdd},
{RoleID: 2, PowerType: permission.TagEdit},
{RoleID: 2, PowerType: permission.TagEditSlugName},
{RoleID: 2, PowerType: permission.TagEditWithoutReview},
{RoleID: 2, PowerType: permission.TagDelete},
{RoleID: 2, PowerType: permission.TagSynonym},
{RoleID: 2, PowerType: permission.LinkUrlLimit},
{RoleID: 2, PowerType: permission.VoteDetail},
{RoleID: 2, PowerType: permission.AnswerAudit},
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 3, PowerType: permission.QuestionDelete},
{RoleID: 3, PowerType: permission.QuestionClose},
{RoleID: 3, PowerType: permission.QuestionReopen},
{RoleID: 3, PowerType: permission.QuestionVoteUp},
{RoleID: 3, PowerType: permission.QuestionVoteDown},
{RoleID: 3, PowerType: permission.AnswerAdd},
{RoleID: 3, PowerType: permission.AnswerEdit},
{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 3, PowerType: permission.AnswerDelete},
{RoleID: 3, PowerType: permission.AnswerAccept},
{RoleID: 3, PowerType: permission.AnswerVoteUp},
{RoleID: 3, PowerType: permission.AnswerVoteDown},
{RoleID: 3, PowerType: permission.CommentAdd},
{RoleID: 3, PowerType: permission.CommentEdit},
{RoleID: 3, PowerType: permission.CommentDelete},
{RoleID: 3, PowerType: permission.CommentVoteUp},
{RoleID: 3, PowerType: permission.CommentVoteDown},
{RoleID: 3, PowerType: permission.ReportAdd},
{RoleID: 3, PowerType: permission.TagAdd},
{RoleID: 3, PowerType: permission.TagEdit},
{RoleID: 3, PowerType: permission.TagEditSlugName},
{RoleID: 3, PowerType: permission.TagEditWithoutReview},
{RoleID: 3, PowerType: permission.TagDelete},
{RoleID: 3, PowerType: permission.TagSynonym},
{RoleID: 3, PowerType: permission.LinkUrlLimit},
{RoleID: 3, PowerType: permission.VoteDetail},
{RoleID: 3, PowerType: permission.AnswerAudit},
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
}
// insert default powers
for _, rel := range rolePowerRels {
exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Context(ctx).Insert(rel)
if err != nil {
return err
}
}
adminUserRoleRel := &entity.UserRoleRel{
UserID: "1",
RoleID: 2,
}
exist, err := x.Context(ctx).Get(adminUserRoleRel)
if err != nil {
return err
}
if !exist {
_, err = x.Context(ctx).Insert(adminUserRoleRel)
if err != nil {
return err
}
}
defaultConfigTable := []*entity.Config{
{ID: 115, Key: "rank.question.close", Value: `-1`},
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return nil
}
+68
View File
@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func addThemeAndPrivateMode(ctx context.Context, x *xorm.Engine) error {
loginConfig := map[string]bool{
"allow_new_registrations": true,
"login_required": false,
}
loginConfigDataBytes, _ := json.Marshal(loginConfig)
siteInfo := &entity.SiteInfo{
Type: "login",
Content: string(loginConfigDataBytes),
Status: 1,
}
exist, err := x.Context(ctx).Get(&entity.SiteInfo{Type: siteInfo.Type})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
_, err = x.Context(ctx).Insert(siteInfo)
if err != nil {
return fmt.Errorf("insert site info failed: %w", err)
}
}
themeConfig := fmt.Sprintf(`{"theme":"default","theme_config":{"default":{"navbar_style":"#0033ff","primary_color":"#0033ff"}},"layout":"%s"}`, constant.ThemeLayoutFullWidth)
themeSiteInfo := &entity.SiteInfo{
Type: "theme",
Content: themeConfig,
Status: 1,
}
exist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: themeSiteInfo.Type})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
_, err = x.Context(ctx).Insert(themeSiteInfo)
}
return err
}
+61
View File
@@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/entity"
"xorm.io/xorm"
)
func addNewAnswerNotification(ctx context.Context, x *xorm.Engine) error {
cond := &entity.Config{Key: "email.config"}
exists, err := x.Context(ctx).Get(cond)
if err != nil {
return fmt.Errorf("get email config failed: %w", err)
}
if !exists {
// This should be impossible except that the config was deleted manually by user.
_, err = x.Context(ctx).Insert(&entity.Config{
Key: "email.config",
Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"}`,
})
if err != nil {
return fmt.Errorf("add email config failed: %v", err)
}
}
m := make(map[string]any)
_ = json.Unmarshal([]byte(cond.Value), &m)
m["new_answer_title"] = "[{{.SiteName}}] {{.DisplayName}} answered your question"
m["new_answer_body"] = "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
m["new_comment_title"] = "[{{.SiteName}}] {{.DisplayName}} commented on your post"
m["new_comment_body"] = "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
val, _ := json.Marshal(m)
_, err = x.Context(ctx).ID(cond.ID).Update(&entity.Config{Value: string(val)})
if err != nil {
return fmt.Errorf("update email config failed: %v", err)
}
return nil
}
+50
View File
@@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addPlugin(ctx context.Context, x *xorm.Engine) error {
defaultConfigTable := []*entity.Config{
{ID: 118, Key: "plugin.status", Value: `{}`},
}
for _, c := range defaultConfigTable {
exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
continue
}
if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return x.Context(ctx).Sync(new(entity.PluginConfig), new(entity.UserExternalLogin))
}
+141
View File
@@ -0,0 +1,141 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"time"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error {
powers := []*entity.Power{
{ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"},
{ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"},
{ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"},
{ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"},
}
// insert default powers
for _, power := range powers {
exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID})
if err != nil {
return err
}
if exist {
_, err = x.Context(ctx).ID(power.ID).Update(power)
} else {
_, err = x.Context(ctx).Insert(power)
}
if err != nil {
return err
}
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.QuestionPin},
{RoleID: 2, PowerType: permission.QuestionHide},
{RoleID: 2, PowerType: permission.QuestionUnPin},
{RoleID: 2, PowerType: permission.QuestionShow},
{RoleID: 3, PowerType: permission.QuestionPin},
{RoleID: 3, PowerType: permission.QuestionHide},
{RoleID: 3, PowerType: permission.QuestionUnPin},
{RoleID: 3, PowerType: permission.QuestionShow},
}
// insert default powers
for _, rel := range rolePowerRels {
exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Context(ctx).Insert(rel)
if err != nil {
return err
}
}
defaultConfigTable := []*entity.Config{
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
type Question struct {
ID string `xorm:"not null pk BIGINT(20) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 INT(11) status"`
Pin int `xorm:"not null default 1 INT(11) pin"`
Show int `xorm:"not null default 1 INT(11) show"`
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
AnswerCount int `xorm:"not null default 0 INT(11) answer_count"`
CollectionCount int `xorm:"not null default 0 INT(11) collection_count"`
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
err := x.Context(ctx).Sync(new(Question))
if err != nil {
return err
}
return nil
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package migrations
import (
"context"
"fmt"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func updateAcceptAnswerRank(ctx context.Context, x *xorm.Engine) error {
c := &entity.Config{ID: 44, Key: "rank.answer.accept", Value: `-1`}
if _, err := x.Context(ctx).Update(c, &entity.Config{ID: 44, Key: "rank.answer.accept"}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
return nil
}