블로그 댓글 시스템 disqus에서 utterances로 바꾸기

  • disqus에 달린 댓글을 utterances가 읽을 수 있게 이슈 생성하기
  • 자문자답하는 것처럼 하나의 계정으로 달리지만 댓글이 없어지지 않는 게 의미가 크다..!

  • 이런 형식이 utterances가 사용하는 포맷이다
  • 이 포맷과 동일하게 수동으로 이슈를 등록하는 것도 좋은? 방법이다

하는 법

환경

  • windows 10
  • 나는 hexo, icarus theme를 사용하고 있다
  • hexo version ; 5.0.0
  • hexo-theme-icarus version ; 4.0.1

disqus 기존 댓글 가져오기

  • 블로그에 댓글 몇 개 없지만 없는 만큼 너무 소중해서 같이 옮긴다

https://lazywinadmin.com/2019/04/moving_blog_comments.html

  • 위 글을 따라가면서 블로그 댓글 시스템 disqus에서 utterances로 바꿔보자
  • 위 글에서는 파워셸을 통해서 export 한 xml을 조작해서 utterances에서 사용하는 이슈로 등록해준다

  • disqus에 로그인한 상태로
  • https://chinsung.disqus.com/admin/discussions/export/
  • 위 링크로 접속해서 버튼을 누르면 된다
  • 버튼을 누르면 내 요청이 큐가 되었다고 하면서 결과를 이메일로 보내준다고 한다
  • 나 같은 경우 바로 이메일이 왔다

  • 링크를 눌러 다운로드하자
  • 압축을 풀면 xml 파일이 나온다
  • 앞으로의 내용은 이 xml 파일을 읽어서 작업한다

disqus 댓글 정제하기

  • 참고로 나는 파워셸 커맨드를 잘 모른다
  • 그대로 따라 하다가 내가 겪었던 문제들이 몇 가지 있었다
  • 원본 글에서는 커맨드에 대해 블록을 나눠서 잘 설명해준다
  • 나는 설명보다는 내가 어떤 문제를 만나서 코드를 어떻게 수정했고
  • 성공한 코드를 마지막에 통짜로 첨부하겠다
  • 파워셸을 관리자 권한으로 실행한다
  • 편한 작업을 위해 cd 명령으로 xml이 있는 위치로 이동한다
  • 나는 파워셸 스크립트(foo.ps1)를 만들어서 실행하는 방법으로 했다
  • 실행은 .\myPsScript.ps1 이렇게 앞에 .\을 붙여 실행할 수 있다

파워셸 시크립트 실행 활성화

  • 파워셸 스크립트를 실행하려면 실행 정책을 변경해야 한다
1
2
Set-ExecutionPolicy AllSigned
Set-ExecutionPolicy RemoteSigned
  • 변경하시겠습니까? 물어보면 Y로 답하면 된다

인코딩 문제

  • 파일을 읽는 것부터 실패했다
new1.ps1
1
2
3
4
5
6
7
8
9
# Load the file
# $Disqus = Get-Content -Path .\origin.xml
$Disqus = Get-Content -Path .\origin.xml -encoding UTF8 # 여기 -encoding UTF8 추가

# Cast the file to XML format
$DisqusXML = ([xml]$Disqus).disqus

# Output result
$DisqusXML
  • -encoding UTF8을 추가해준다

필터링 문제

1
2
3
4
5
6
7
8
9
10
$AllThreads = $AllThreads |
Where-Object -FilterScript {
$_.link -match "
\.io\/\d{4}\/.+html$|
\.com\/\d{4}\/.+html$|
\.com\/p\/.+html$|
\.io\/minimal-mistakes\/\d{4}\/.+html$|
\.io\/powershell\/\d{4}\/.+html$|
\.io\/usergroup\/\d{4}\/.+html$" -and
$_.link -notmatch "googleusercontent\.com"}
  • 문제까지는 아니고, 글을 잘 안 읽고 그냥 코드를 복붙하다 보니까
  • 글쓴이에 상황에 맞춰진 조건을 그대로 사용해 생긴 문제였다
  • 저 코드를 내 상황에서 돌리면 모든 쓰레드가 조건에 충족하지 않아 결과가 빈 배열이다
  • 나는 필터링이 필요하지 않았다 그래서 이 과정은 생략했다

개인 설정 문제

1
2
3
4
5
6
# Define Github commands default params
$GithubSplat = @{
OwnerName = 'lazywinadmin'
RepositoryName = 'lazywinadmin.github.io'
}
$BlogUrl = 'https://lazywinadmin.com'
  • 코드 좀 보고 복붙하자…
  • 남의 레포를 업데이트할 권한이 없어서 망정이지…
1
2
3
4
5
6
# Define Github commands default params
$GithubSplat = @{
OwnerName = 'chinsun9'
RepositoryName = 'chinsun9.github.io'
}
$BlogUrl = 'https://chinsun9.github.io/' # 주의! 마지막 슬래시 넣기!
  • 내 환경에 맞게 적절히 수정해준다

github personal access token 발급 받기

  • 파워셸에서 github api를 통해 이슈를 자동 생성한다
  • github api를 사용하기 위해서는 토큰이 필요하다
  • github에 로그인한 상태로
  • https://github.com/settings/tokens
  • 에 접속해서 Generate new token 버튼을 누른다

  • note를 적당히 작성하고
  • repo 전체 권한을 가지도록 생성한다
  • 생성된 키를 복사한다

완성 코드

  • 내가 사용한 코드이다
  • 실행할 때는 관리자 권한으로 파워셸을 실행시켜야 한다
  • 가장 상위에 개인이 설정해야 하는 변수들을 몰아놨다
  • 해당 변수들은 전부 자신에 맞게 수정한 다음 실행해보아야 한다
  • 특히 RepositoryName은 일회용으로 사용할 레포를 생성하고 한번 테스트해보는 것을 추천한다
  • 혹시나 잘못된 이슈가 엄청나게 생성될 수 있기 때문이다
  • 일회용 레포를 하나 만들고 테스트해보고 진짜 레포에 적용하는 게 좋을 것 같다
  • 파워셸에서 github api를 사용하기 위한 powershellforgithub 모듈을 다운로드하는 코드가 4번째 라인에 있다
  • 코드를 실행하면 뭐라 뭐라 설치할 건지 물어보는데 모두 Y로 답하면 된다
  • utterances는 공개 레포에서 작동하니까 별도 레포를 만들지 않고,
  • 블로그의 본체인 chinsun9.github.io 레포를 그냥 사용했다
complete code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# 출처 ; https://lazywinadmin.com/2019/04/moving_blog_comments.html

# First fetch the module from the PowerShell Gallery
Install-Module -Name powershellforgithub -scope currentuser -verbose
# Import it
Import-Module -Name powershellforgithub

### 개인 설정 시작 ###
$filePath = '.\origin.xml' # disqus export xml filepath

# Define Github commands default params
$GithubSplat = @{
OwnerName = 'chinsun9' # github username
RepositoryName = 'chinsun9.github.io' # reponame ; 처음 테스트할 땐 일회용 레포하나 만들고 결과가 어떻게 나오나 확인해자
}
$BlogUrl = 'https://chinsun9.github.io/' # blog url ; 마지막 슬래시 있어야함!

# Specify our Github Token
$key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' # 깃허브에서 발급한 personal access token!!

$IssueLabel = 'blog comments' # 라벨 설정
### 개인 설정 끝 ###

# Load the file
$Disqus = Get-Content -Path $filePath -encoding UTF8

# Cast the file to XML format
$DisqusXML = ([xml]$Disqus).disqus


# 코멘트 작업
# Retrieve all Comments
$AllComments = $DisqusXML.post

# Retrieve properties available for each comments
$Properties = $AllComments | Get-Member -MemberType Property

# Process each Comments
$AllComments = $AllComments | Foreach-Object -process {

# Store the current comment
$Comment = $_

# Create Hashtable to store properties of the current comment
$Post = @{}

# Go through each properties of each comments
foreach ($prop in $Properties.name) {
if ($prop -eq 'id') {
# Capture Unique IDs
$Post.DsqID = $Comment.id[0]
$Post.ID = $Comment.id[1]
}
elseif ($prop -eq 'author') {
# Author information
$Post.AuthorName = $Comment.author.name
$Post.AuthorIsAnonymous = $Comment.author.isanonymous
}
elseif ($prop -eq 'thread') {
# Here is the important data about the
# thread the comment belong to
$Post.ThreadId = $Comment.thread.id
}
elseif ($prop -eq 'message') {
$Post.Message = $Comment.message.'#cdata-section'
}
else {
# Other properties
$Post.$prop = ($Comment |
Select-Object -ExpandProperty $prop ) -replace '`r`n'
}
# Keep the original comment data structure if we need it later
$Post.raw = $Comment
}
# Return a PowerShell object for the current comment
New-Object -TypeName PSObject -Property $Post
}

# 쓰레드 작업
# Retrieve threads
$AllThreads = $DisqusXML.thread

# Retrieve Thread properties
$Properties = $AllThreads | Get-Member -MemberType Property

# Process each threads
$AllThreads = $AllThreads | Foreach-Object -process {

# Capture Current ThreadItem
$ThreadItem = $_

# Create Hashtable for our final object
$ThreadObj = @{}

# Go through each properties of each threads
foreach ($prop in $Properties.name) {
if ($prop -eq 'id') {
# Thread ID
$ThreadObj.ID = $ThreadItem.id[0]
}
elseif ($prop -eq 'author') {
# Author
$ThreadObj.AuthorName = $ThreadItem.author.name
$ThreadObj.AuthorIsAnonymous = $ThreadItem.author.isanonymous
$ThreadObj.AuthorUsername = $ThreadItem.author.username
}
elseif ($prop -eq 'message') {
$ThreadObj.Message = $ThreadItem.message.'#cdata-section'
}
elseif ($prop -eq 'category') {
$ThreadObj.Category = ($ThreadItem |
Select-Object -ExpandProperty $prop).id
}
else {
# Other properties
$ThreadObj.$prop = ($ThreadItem |
Select-Object -ExpandProperty $prop) -replace '`r`n'
}
$ThreadObj.raw = $ThreadItem
}
# Return a PowerShell object for the current ThreadItem
New-Object -TypeName PSObject -Property $ThreadObj
}

$AllThreads = $AllThreads |
Select-Object -Property *,
@{L = 'link2'; E = {
$_.link -replace "$($BlogUrl)" }
},
@{L = 'title2'; E = {
$_.title }
} |
Group-Object -Property link2


$ThreadsUpdated = $AllThreads |
Sort-Object -Property count |
ForEach-Object -Process {

# Capture current post
$CurrentPost = $_

# if one comment is found
if ($CurrentPost.count -eq 1) {
if ($CurrentPost.group.title2 -notmatch '^http') {
# Add REALTitle property
$RealTitle = $CurrentPost.group.title2
# output object
$CurrentPost.group |
Select-Object -Property *,
@{L = 'RealTitle'; e = { $RealTitle } },
@{L = 'ThreadCount'; e = { $CurrentPost.count } }
}
elseif ($CurrentPost.group.title2 -match '^http') {
# lookup online
$result = Invoke-webrequest -Uri $CurrentPost.group.link -Method Get
# add REALTitle prop
$RealTitle = $result.ParsedHtml.title
# output object
$CurrentPost.group |
Select-Object -Property *,
@{L = 'RealTitle'; e = { $RealTitle } },
@{L = 'ThreadCount'; e = { $CurrentPost.count } }
}
}
if ($CurrentPost.count -gt 1) {
if ($CurrentPost.group.title2 -notmatch '^http') {
# add REALTitle prop
$RealTitle = ($CurrentPost.group.title2 |
Where-Object -FilterScript {
$_ -notmatch '^http' } |
Select-Object -first 1)

# Output object
$CurrentPost.group |
Select-Object -Property *,
@{L = 'RealTitle'; e = { $RealTitle } },
@{L = 'ThreadCount'; e = { $CurrentPost.count }
}
}
elseif ($CurrentPost.group.title2 -match '^http') {
# get url of one
$u = ($CurrentPost.group |
Where-Object {
$_.title2 -match '^http' } |
Select-Object -first 1).link

# lookup online
$result = Invoke-webrequest -Uri $u -Method Get

# add REALTitle prop
$RealTitle = $result.ParsedHtml.title
# output object
$CurrentPost.group |
Select-Object *, @{L = 'RealTitle'; e = { $RealTitle }
}
}
else {
# add REALTitle prop
$RealTitle = 'unknown'
# output object
$CurrentPost.group | Select-Object *, @{L = 'RealTitle'; e = { $RealTitle } }
}
}
}


$AllTogether = $AllComments | ForEach-Object -Process {

$CommentItem = $_


$ThreadInformation = $ThreadsUpdated |
Where-Object -FilterScript {
$_.id -match $CommentItem.ThreadId
}

$CommentItem |
Select-Object -Property *,
@{L = 'ThreadTitle'; E = { $ThreadInformation.Realtitle } },
@{L = 'ThreadLink'; E = { $ThreadInformation.link2 } }
} |
Group-Object -Property ThreadLink |
Where-Object -FilterScript { $_.name }


# 이슈달기 작업
$KeySec = ConvertTo-SecureString $key -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential ('username_is_ignored', $KeySec)
#$cred = Get-Credential -UserName $null

# Set Connection and configuration
Set-GitHubAuthentication -Credential $cred
Set-GitHubConfiguration -DisableLogging -DisableTelemetry


# Retrieve issues
#$issues = Get-GitHubIssue -Uri 'https://github.com/lazywinadmin/lazywinadmin.github.io'
$issues = Get-GitHubIssue @githubsplat

# Process each threads with their comments
$AllTogether |
Sort-Object name -Descending |
ForEach-Object -Process {

# Capture current thread
$BlogPost = $_

# Issue Title, replace the first / and
# remove the html at the end of the name
$IssueTitle = $BlogPost.group.ThreadLink |
select-object -first 1

# lookup for existing issue
$IssueObject = $issues |
Where-Object -filterscript { $_.title -eq $IssueTitle }

if (-not $IssueObject) {
# Build Header of the post
$IssueHeader = $BlogPost.group.ThreadTitle |
select-object -first 1

# Define blog post link
$BlogPostLink = "$($BlogUrl)$($BlogPost.name)"

# Define body of the issue
$Body = @"
# $IssueHeader

[$BlogPostLink]($BlogPostLink)

<!--
Imported via PowerShell on $(Get-Date -Format o)
-->
"@
# Create an issue
$IssueObject = New-GitHubIssue @githubsplat `
-Title $IssueTitle `
-Body $body `
-Label $IssueLabel
}

# Sort comment by createdAt
$BlogPost.group |
Where-Object { $_.isspam -like '*false*' } |
Sort-Object createdAt |
ForEach-Object {

# Current comment
$CurrenComment = $_

# Author update
# we replace my post author name :)
$AuthorName = $($CurrenComment.AuthorName)
switch ($AuthorName) {
'Xavier C' { $AuthorName = 'Francois-Xavier Cat' }
default {}
}

# Define body of the comment
$CommentBody = @"

## **Author**: $AuthorName
**Posted on**: ``$($CurrenComment.createdAt)``
$($CurrenComment.message)

<!--
Imported via PowerShell on $(Get-Date -Format o)
Json_original_message:
$($CurrenComment|Select-Object -ExcludeProperty raw|convertTo-Json)
-->
"@
# Create Comment
New-GitHubComment @githubsplat `
-Issue $IssueObject.number `
-Body $CommentBody
}
# Close issue
Update-GitHubIssue @githubsplat `
-Issue $IssueObject.number `
-State Closed
}

utterances 설정하기

  • 난 특정 레포에만 설치했다
  • 여기 설정은 언제든지 수정 가능하다
  • 놀랍게도 끝났다
  • 내가 사용하는 icarus 테마는 댓글로 discus, utterances 등 여러 타입의 플러그인을 지원해서 config에 추가하면 적용 완료다

blog config 설정하기

_config.icarus.yml
1
2
3
4
5
6
7
comment:
type: utterances
repo: chinsun9/chinsun9.github.io
issue_term: pathname
label: blog comments
theme: github-light
crossorigin: anonymous
  • 위처럼 설정한다
  • 레포는 퍼블릭 레포여야 한다
  • 내가 사용한 방법을 그대로 따라왔다면 issue_term은 pathname을 사용해야 한다
  • label도 등록된 이슈와 동일하게 설정한다

utterances css 수정하기 ; width 100%

default.styl
1
2
3
.utterances {
max-width: none;
}
  • 각자 테마 디렉터리로 가서 css 파일에 가서 추가한다

주의사항

테마 설정 시

  • hexo icarus config에서 utterances 테마를 설정할 때
  • preferred-color-scheme는 지원하지 않는 것 같다…
  • 이 값이 올 수 없다며 스키마 오류가 난다 (버전 문제일 수 있음)
  • github-dark, github-light은 가능하다

이슈 제목

  • utterances에서 이슈와 포스트를 맵핑할 때 여러 옵션이 있다
  • 맘에 드는 걸로 골르면 된다 (이 글을 그대로 따라왔다면 pathname으로 해야 함!)
  • 블로그 포스트를 작성하고 수정될 가능성이 있는 값으로 지정하지 않은 게 좋다
  • 나는 첫 번째 옵션인 Issue title contains page pathname으로 매핑하기로 했다
  • 참고로 내 블로그 pathname은 /YYYY/MM/DD/post-title 로 구성되어 유니크한 이름을 가질 수 있다
  • 포스트의 pathname에 한글이 들어가면 이상한 인코딩 된 이슈 제목을 가진다
  • 앞으로 pathname에 한글이 들어가지 않도록 글을 써야겠다…

  • 이렇게 되기 싫으면…

바꾼 이유

  • disqus를 사용하면 링크가 넣을 수 없다?
  • 댓글에 포함되어 있는 링크가 disqus를 통해 리디렉트되도록
  • https://disq.us/url?url=https%3A%2F%2Fchinsun9.github.io%2F2020%2F11%2F18%2Freact-typescirpt...
  • 이렇게 감싸 진다
  • 근데 문제는 이렇게 감싸진 링크를 클릭하면 연결이 안 된다
  • 내가 무슨 설정을 잘못한 건지 모르겠지만…
  • 또, 나는 disqus를 블로그를 하기 전까지 모르고 있었다
  • 댓글을 달기 위해서 disqus를 가입해야 하는 벽?이 있지 않을까 생각하게 되었다

참고