Skip to content

Commit

Permalink
perf(gravatar): support both sha256 and md5 hashing algorithm for gra…
Browse files Browse the repository at this point in the history
…vatar
  • Loading branch information
qwqcode committed Jun 8, 2024
1 parent c3558b7 commit 70cece7
Show file tree
Hide file tree
Showing 14 changed files with 129 additions and 31 deletions.
2 changes: 1 addition & 1 deletion conf/artalk.example.simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ frontend:
nestSort: DATE_ASC
gravatar:
mirror: https://www.gravatar.com/avatar/
params: d=mp&s=240
params: sha256=1&d=mp&s=240
pagination:
pageSize: 20
readMore: true
Expand Down
2 changes: 1 addition & 1 deletion conf/artalk.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ frontend:
# API URL
mirror: https://www.gravatar.com/avatar/
# API parameters
params: d=mp&s=240
params: sha256=1&d=mp&s=240
# Comment pagination
pagination:
# Number of comments per page
Expand Down
4 changes: 2 additions & 2 deletions conf/artalk.example.zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ frontend:
# 头像 Gravatar
gravatar:
# API 地址
mirror: https://cravatar.cn/avatar/
mirror: https://weavatar.com/avatar/
# API 参数
params: d=mp&s=240
params: sha256=1&d=mp&s=240
# 评论分页
pagination:
# 每页评论数
Expand Down
3 changes: 0 additions & 3 deletions docs/docs/.vitepress/theme/Artalk.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ function initArtalk(conf: any) {
artalk = Artalk.init({
el: el.value,
emoticons: '/assets/emoticons/default.json',
gravatar: {
mirror: 'https://cravatar.cn/avatar/',
},
...conf,
})
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/guide/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,8 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
| **ATK_FRONTEND_EDITORTRAVEL** | `true` | 评论框穿梭 | frontend.editorTravel (界面配置 > 评论框穿梭) |
| **ATK_FRONTEND_EMOTICONS** | `"https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json"` | 表情包 | frontend.emoticons (界面配置 > 表情包) |
| **ATK_FRONTEND_FLATMODE** | `"auto"` | 平铺模式 (可选:`["auto", "true", "false"]`) | frontend.flatMode (界面配置 > 平铺模式) |
| **ATK_FRONTEND_GRAVATAR_MIRROR** | `"https://cravatar.cn/avatar/"` | API 地址 | frontend.gravatar.mirror (界面配置 > 头像 Gravatar > API 地址) |
| **ATK_FRONTEND_GRAVATAR_PARAMS** | `"d=mp&s=240"` | API 参数 | frontend.gravatar.params (界面配置 > 头像 Gravatar > API 参数) |
| **ATK_FRONTEND_GRAVATAR_MIRROR** | `"https://weavatar.com/avatar/"` | API 地址 | frontend.gravatar.mirror (界面配置 > 头像 Gravatar > API 地址) |
| **ATK_FRONTEND_GRAVATAR_PARAMS** | `"sha256=1&d=mp&s=240"` | API 参数 | frontend.gravatar.params (界面配置 > 头像 Gravatar > API 参数) |
| **ATK_FRONTEND_HEIGHTLIMIT_CHILDREN** | `400` | 子评论区域限高 (单位:px) | frontend.heightLimit.children (界面配置 > 内容限高 > 子评论区域限高) |
| **ATK_FRONTEND_HEIGHTLIMIT_CONTENT** | `300` | 评论内容限高 (单位:px) | frontend.heightLimit.content (界面配置 > 内容限高 > 评论内容限高) |
| **ATK_FRONTEND_HEIGHTLIMIT_SCROLLABLE** | `false` | 滚动限高 (允许限高区域滚动) | frontend.heightLimit.scrollable (界面配置 > 内容限高 > 滚动限高) |
Expand Down
8 changes: 6 additions & 2 deletions docs/docs/guide/frontend/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,14 @@ gravatar: {
**Gravatar 镜像地址**

- 类型:`String`
- 默认值:`"https://sdn.geekzu.org/avatar/"`
- 默认值:`"https://www.gravatar.com/avatar/"`

如果你觉得 Gravatar 头像加载速度不理想,可以尝试替换。

例如:

> WeAvatar:https://weavatar.com/avatar/
>
> Cravatar:https://cravatar.cn/avatar/
>
> V2EX:https://cdn.v2ex.com/gravatar/
Expand All @@ -377,7 +379,7 @@ gravatar: {
**Gravatar API 参数**

- 类型:`String`
- 默认值:`"d=mp&s=240"`
- 默认值:`"sha256=1&d=mp&s=240"`

例如,你可以通过该配置项设置默认头像 (`d=mp`) 和头像尺寸 (`s=240`)。

Expand All @@ -387,6 +389,8 @@ gravatar: {

::: warning 更新注意

v2.8.7 支持通过 SHA256 加密算法生成 Gravatar 头像链接,以提高用户隐私保护,请在 `params` 中填入 `sha256=1` 启用该加密([#874](https://github.com/ArtalkJS/Artalk/issues/874))。

v2.5.5 已废弃 `gravatar.default` 配置项,请使用 `gravatar.params` 替代。

:::
Expand Down
2 changes: 1 addition & 1 deletion internal/config/cache.go

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions internal/config/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package config

import (
"strings"

"github.com/ArtalkJS/Artalk/internal/utils"
)

// Get the hash function according to the frontend gravatar.params configuration
func GetHashFuncByFrontendConf(conf *Config) func(string) string {
if gravatar, ok := conf.Frontend["gravatar"].(map[string]interface{}); ok {
if params, ok := gravatar["params"].(string); ok {
if strings.Contains(params, "sha256=1") {
return utils.GetSha256Hash
}
}
}
return utils.GetMD5Hash
}
51 changes: 51 additions & 0 deletions internal/config/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package config

import (
"reflect"
"testing"

"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/stretchr/testify/assert"
)

func Test_GetHashFuncByFrontendConf(t *testing.T) {
tests := map[string]struct {
config *Config
expected any
}{
"nil frontend conf": {
config: &Config{Frontend: nil},
expected: utils.GetMD5Hash,
},
"empty frontend conf": {
config: &Config{Frontend: map[string]interface{}{}},
expected: utils.GetMD5Hash,
},
"frontend conf without hash func": {
config: &Config{Frontend: map[string]interface{}{
"gravatar": false,
}},
expected: utils.GetMD5Hash,
},
"frontend conf with gravatar params but without hash func": {
config: &Config{Frontend: map[string]interface{}{
"gravatar": map[string]interface{}{"params": "a=1&b=2"},
}},
expected: utils.GetMD5Hash,
},
"frontend conf with gravatar params containing hash func": {
config: &Config{Frontend: map[string]interface{}{
"gravatar": map[string]interface{}{"params": "a=1&sha256=1&b=2"},
}},
expected: utils.GetSha256Hash,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
expect := reflect.ValueOf(tc.expected).Pointer()
actual := reflect.ValueOf(GetHashFuncByFrontendConf(tc.config)).Pointer()
assert.Equal(t, expect, actual)
})
}
}
15 changes: 10 additions & 5 deletions internal/core/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ func NewApp(conf *config.Config) *App {
}

func (app *App) injectDefaultServices() {
// 请勿依赖注入顺序
AppInject[*EmailService](app, NewEmailService(app))
AppInject[*IPRegionService](app, NewIPRegionService(app))
AppInject[*NotifyService](app, NewNotifyService(app))
AppInject[*AntiSpamService](app, NewAntiSpamService(app))
// Please do not depend on the order of dependency injection
AppInject(app, NewEmailService(app))
AppInject(app, NewIPRegionService(app))
AppInject(app, NewNotifyService(app))
AppInject(app, NewAntiSpamService(app))
}

func (app *App) registerDefaultHooks() {
Expand Down Expand Up @@ -270,6 +270,11 @@ func (app *App) initDao() error {
// create new dao instance
app.SetDao(dao.NewDao(dbInstance))

// patch: switch comment email hash algorithm by config
app.Dao().SetCommentEmailHashFunc(func(email string) string {
return config.GetHashFuncByFrontendConf(app.Conf())(strings.ToLower(email))
})

Check warning on line 276 in internal/core/base.go

View check run for this annotation

Codecov / codecov/patch

internal/core/base.go#L275-L276

Added lines #L275 - L276 were not covered by tests

return nil
}

Expand Down
13 changes: 11 additions & 2 deletions internal/dao/cook.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Convert Entity to JSON Response Data Structure for API
// TODO: (refactor) consider to extract this file to a new package
package dao

import (
Expand All @@ -10,6 +12,13 @@ import (

const CommonDateTimeFormat = "2006-01-02 15:04:05"

// TODO: (refactor) remove this global variable
var getCommentEmailHash = func(email string) string { return utils.GetMD5Hash(strings.ToLower(email)) }

func (dao *Dao) SetCommentEmailHashFunc(fn func(string) string) {
getCommentEmailHash = fn

Check warning on line 19 in internal/dao/cook.go

View check run for this annotation

Codecov / codecov/patch

internal/dao/cook.go#L18-L19

Added lines #L18 - L19 were not covered by tests
}

// ===============
// Comment
// ===============
Expand Down Expand Up @@ -40,7 +49,7 @@ func (dao *Dao) CookComment(c *entity.Comment) entity.CookedComment {
ContentMarked: markedContent,
UserID: c.UserID,
Nick: user.Name,
EmailEncrypted: utils.GetSha256Hash(strings.ToLower(user.Email)),
EmailEncrypted: getCommentEmailHash(user.Email),
Link: user.Link,
UA: c.UA,
Date: c.CreatedAt.Local().Format(CommonDateTimeFormat),
Expand Down Expand Up @@ -92,7 +101,7 @@ func (dao *Dao) CookCommentForEmail(c *entity.Comment) entity.CookedCommentForEm
Site: dao.CookSite(&site),
CookedComment: entity.CookedComment{
ID: c.ID,
EmailEncrypted: utils.GetSha256Hash(strings.ToLower(user.Email)),
EmailEncrypted: getCommentEmailHash(user.Email),

Check warning on line 104 in internal/dao/cook.go

View check run for this annotation

Codecov / codecov/patch

internal/dao/cook.go#L104

Added line #L104 was not covered by tests
Link: user.Link,
UA: c.UA,
IsCollapsed: c.IsCollapsed,
Expand Down
13 changes: 3 additions & 10 deletions ui/artalk-sidebar/src/components/Header.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
<script setup lang="ts">
import sha256 from 'crypto-js/sha256'
import { storeToRefs } from 'pinia'
import { useUserStore } from '../stores/user'
import { useNavStore } from '../stores/nav'
import { artalk, isOpenFromSidebar } from '../global'
import { isOpenFromSidebar } from '../global'
const nav = useNavStore()
const router = useRouter()
const user = useUserStore()
const { t } = useI18n()
const { site: curtSite, isAdmin, email } = storeToRefs(user)
const userAvatarImgURL = computed(() => {
const conf = artalk?.ctx.conf?.gravatar
if (!conf) return ``
return `${conf.mirror.replace(/\/$/, '')}/${sha256(email.value.toLowerCase())}?${conf.params.replace(/^\?/, '')}`
})
const { site: curtSite, isAdmin, avatar } = storeToRefs(user)
const avatarClickHandler = () => {
if (!isOpenFromSidebar()) logout()
Expand Down Expand Up @@ -44,7 +37,7 @@ const logout = () => {
</template>
<template v-else>
<div class="avatar" @click="avatarClickHandler">
<img :src="userAvatarImgURL" />
<img :src="avatar" />
</div>
</template>

Expand Down
20 changes: 20 additions & 0 deletions ui/artalk-sidebar/src/stores/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { defineStore } from 'pinia'
import { bootParams, getArtalk } from '../global'
import sha256 from 'crypto-js/sha256'
import md5 from 'crypto-js/md5'

export const useUserStore = defineStore('user', {
state: () => ({
Expand Down Expand Up @@ -31,4 +33,22 @@ export const useUserStore = defineStore('user', {
this.token = userData.token
},
},
getters: {
avatar: (state) => {
return getGravatar(state.email)
},
},
})

function getGravatar(email: string) {
// TODO get avatar url from backend api
const conf = getArtalk()?.ctx.conf?.gravatar
if (!conf) return ''

const emailHash =
typeof conf.params == 'string' && conf.params.includes('sha256=1')
? sha256(email.toLowerCase()).toString()
: md5(email.toLowerCase()).toString()

return `${conf.mirror.replace(/\/$/, '')}/${emailHash}?${conf.params.replace(/^\?/, '')}`
}
4 changes: 2 additions & 2 deletions ui/artalk/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ const defaults: ArtalkConfig = {
statPageKeyAttr: 'data-page-key',

gravatar: {
mirror: 'https://cravatar.cn/avatar/',
params: 'd=mp&s=240',
mirror: 'https://www.gravatar.com/avatar/',
params: 'sha256=1&d=mp&s=240',
},

pagination: {
Expand Down

0 comments on commit 70cece7

Please sign in to comment.