diff --git a/README.md b/README.md index b902ef87..d2a6bbd7 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,8 @@ We use dimension `Message Source` (Custom Dimemsion1) to classify different even - `UserInput` / `ArticleSearch` / `ArticleFoundButNoHit` - When user provides source - `UserInput` / `IsForwarded` / `Yes` | `No` + - When user specifies if the message comes from same person at same time (cooccurrence) + - `UserInput` / `IsCooccurrence` / `Yes` | `No` - Matches one of Dialogflow intents - `UserInput` / `ChatWithBot` / `` diff --git a/i18n/ja.po b/i18n/ja.po index e4d42022..c05682aa 100644 --- a/i18n/ja.po +++ b/i18n/ja.po @@ -6,11 +6,11 @@ msgstr "" "mime-version: 1.0\n" "Content-Transfer-Encoding: 8bit\n" -#: src/webhook/handlers/utils.ts:85 +#: src/webhook/handlers/utils.ts:94 msgid "No feedback yet" msgstr "まだリプライはありません" -#: src/webhook/handlers/utils.ts:99 +#: src/webhook/handlers/utils.ts:108 msgid "${ negative } user consider this not useful" msgid_plural "${ negative } users consider this not useful" msgstr[0] "${negative} 人がこのリプライは役に立っていないと思っています" @@ -31,47 +31,45 @@ msgstr "個人の意見が含まれています" msgid "Invalid request" msgstr "検証の範囲外" -#: src/webhook/handlers/utils.ts:126 +#: src/webhook/handlers/utils.ts:135 msgid "different opinions" msgstr "違う観点" -#: src/webhook/handlers/utils.ts:126 +#: src/webhook/handlers/utils.ts:135 msgid "references" msgstr "情報源" -#: src/webhook/handlers/utils.ts:128 -msgid "replied at" -msgstr "に返信しました" - -#: src/webhook/handlers/utils.ts:132 +#: src/webhook/handlers/utils.ts:141 #, javascript-format msgid "This reply has no ${ prompt } and it may be biased" msgstr "このリプライは${prompt}がありませんので、信頼性を見直す必要があります。" -#: src/webhook/handlers/choosingArticle.ts:224 +#: src/webhook/handlers/choosingArticle.ts:216 msgid "Volunteer editors have published several replies to this message." msgstr "Cofacts のボランティアがこの情報に多くのリプライをしました!" -#: src/webhook/handlers/choosingArticle.ts:350 +#: src/webhook/handlers/choosingArticle.ts:342 msgid "Let's pick one" msgstr "一つ選んでチェックしましょう" -#: src/webhook/handlers/choosingArticle.ts:327 +#: src/webhook/handlers/choosingArticle.ts:319 msgid "Take a look" msgstr "見てみましょう" -#: src/webhook/handlers/choosingArticle.ts:366 +#: src/webhook/handlers/choosingArticle.ts:358 #, javascript-format msgid "Visit ${ articleUrl } for more replies." msgstr "他のリプライを見たい場合、こちらに:${ articleUrl }" -#: src/webhook/handlers/initState.ts:242 -#: src/webhook/handlers/processMedia.ts:228 +#: src/webhook/handlers/utils.ts:1113 msgid "Choose this one" msgstr "これを選ぶ" -#: src/webhook/handlers/initState.ts:334 -#: src/webhook/handlers/processMedia.ts:321 +#: src/webhook/handlers/askingCooccurrence.ts:185 +#: src/webhook/handlers/initState.ts:188 +#: src/webhook/handlers/processMedia.ts:150 +#. Get first few search results for each message, and make at most 10 options +#. msgid "" "Internet rumors are often mutated and shared.\n" "Please choose the version that looks the most similar" @@ -79,18 +77,18 @@ msgstr "" "デマ情報はよく何度も編集され発送されます。\n" "最も関連するバージョンを選んでください" -#: src/webhook/handlers/utils.ts:649 +#: src/webhook/handlers/utils.ts:658 #, javascript-format msgid "Therefore, the author think the message ${ typeStr }." msgstr "以上により、リプライ者が次のように考えています ${typeStr}。" -#: src/webhook/handlers/utils.ts:652 +#: src/webhook/handlers/utils.ts:661 msgid "" "There are different replies for the message. Read them all here before " "making judgements:" msgstr "この情報に様々なリプライがあるため、読み通してから判断するようにお薦めします:" -#: src/webhook/handlers/utils.ts:654 +#: src/webhook/handlers/utils.ts:663 msgid "If you have different thoughts, you may have your say here:" msgstr "違う見方をお持ちの方は、どうぞ気軽に新規リプライを投稿してください:" @@ -102,70 +100,74 @@ msgstr "以上のリプライは役に立ちましたか?" #: src/liff/components/FeedbackForm.svelte:27 #: src/webhook/handlers/choosingReply.ts:63 +#: src/webhook/handlers/processBatch.ts:31 msgid "Yes" msgstr "はい" #: src/liff/components/FeedbackForm.svelte:30 #: src/webhook/handlers/choosingReply.ts:73 +#: src/webhook/handlers/processBatch.ts:41 msgid "No" msgstr "いいえ" -#: src/webhook/handlers/initState.ts:285 -#: src/webhook/handlers/initState.ts:303 -#: src/webhook/handlers/processMedia.ts:272 -#: src/webhook/handlers/processMedia.ts:290 +#: src/webhook/handlers/initState.ts:143 +#: src/webhook/handlers/initState.ts:161 +#: src/webhook/handlers/processMedia.ts:105 +#: src/webhook/handlers/processMedia.ts:123 msgid "None of these messages matches mine :(" msgstr "確認したい情報が見つかりません (T_T)" -#: src/webhook/handlers/utils.ts:90 +#: src/webhook/handlers/utils.ts:99 #, javascript-format msgid "${ positive } user considers this helpful" msgid_plural "${ positive } users consider this helpful" msgstr[0] "${positive} 人がこのリプライは役に立っていると思っています" -#: src/webhook/handlers/initState.ts:301 -#: src/webhook/handlers/processMedia.ts:288 +#: src/webhook/handlers/initState.ts:159 +#: src/webhook/handlers/processMedia.ts:121 msgid "Tell us more" msgstr "情報を報告する" -#: src/webhook/handlers/initState.ts:317 -#: src/webhook/handlers/processMedia.ts:304 +#: src/webhook/handlers/askingCooccurrence.ts:190 +#: src/webhook/handlers/initState.ts:175 +#: src/webhook/handlers/processMedia.ts:137 +#. Get first few search results for each message, and make at most 10 options +#. msgid "Please choose the most similar message from the list." msgstr "以下から調べたい情報を選んでください。" -#: src/webhook/handlers/choosingArticle.ts:354 +#: src/webhook/handlers/choosingArticle.ts:346 msgid "Please take a look at the following replies." msgstr "以下から調べたいリプライを選んでください。" -#: src/webhook/handlers/choosingArticle.ts:273 +#: src/webhook/handlers/choosingArticle.ts:265 #, javascript-format msgid "Someone thinks it ${ typeWords }" msgstr "誰かが次のように考えています ${typeWords}" -#: src/webhook/handlers/initState.ts:219 -#: src/webhook/handlers/processMedia.ts:140 +#: src/webhook/handlers/utils.ts:1023 #, javascript-format msgid "Looks ${ similarityPercentage }% similar" msgstr "${similarityPercentage}% 似ています" -#: src/webhook/handlers/singleUserHandler.ts:77 +#: src/webhook/handlers/singleUserHandler.ts:82 msgid "" "Line bot is busy, or we cannot handle this message. Maybe you can try again " "a few minutes later." msgstr "申し訳ございません!只今、混み合っているため、この情報をすぐに処理できません。恐れ入りますが、しばらくたってからご利用ください。" -#: src/webhook/handlers/handlePostback.ts:81 +#: src/webhook/handlers/handlePostback.ts:86 msgid "Wrong usage" msgstr "使い方は間違いました" -#: src/webhook/handlers/singleUserHandler.ts:272 +#: src/webhook/handlers/singleUserHandler.ts:283 #. Reuse existing context msgid "" "You are currently searching for another message, buttons from previous " "search sessions do not work now." msgstr "新規の情報を検索しているため、先ほどまでの検索ボタンは無効になります。" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:137 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:192 #, javascript-format msgid "Your submission is now recorded at ${ articleUrl }" msgstr "提供していただいた情報はこちらに登録されています ${ articleUrl }" @@ -206,34 +208,34 @@ msgid "Share to friends" msgstr "友達にシェアする" #: src/webhook/handlers/askingArticleSource.ts:34 -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:50 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:53 +#: src/webhook/handlers/askingCooccurrence.ts:39 #: src/webhook/handlers/choosingArticle.ts:71 #: src/webhook/handlers/choosingReply.ts:152 msgid "Please choose from provided options." msgstr "上記の項目から選んでください。" -#: src/webhook/handlers/choosingArticle.ts:228 +#: src/webhook/handlers/choosingArticle.ts:220 #, javascript-format msgid "${ countOfType.RUMOR } of them say it ❌ contains misinformation." msgstr "${ countOfType.RUMOR } 人がこれは嘘の情報 ❌ が含まれていると思っています。" -#: src/webhook/handlers/choosingArticle.ts:231 +#: src/webhook/handlers/choosingArticle.ts:223 msgid "${ countOfType.NOT_RUMOR } of them says it ⭕ contains true information." msgstr "${ countOfType.NOT_RUMOR } 人がこれは ⭕ 事実が含まれていると思っています。" -#: src/webhook/handlers/choosingArticle.ts:234 +#: src/webhook/handlers/choosingArticle.ts:226 #, javascript-format msgid "" "${ countOfType.OPINIONATED } of them says it 💬 contains personal " "perspective." msgstr "${ countOfType.OPINIONATED } 人がこれは 💬 個人の意見が含まれると思っています。" -#: src/webhook/handlers/choosingArticle.ts:237 +#: src/webhook/handlers/choosingArticle.ts:229 msgid "${ countOfType.NOT_ARTICLE } of them says it ⚠️️ is out of scope of Cofacts." msgstr "${ countOfType.NOT_ARTICLE } 人がこれは ⚠️️ Cofacts 対応対象外だと思っています。" -#: src/webhook/handlers/choosingArticle.ts:329 -#: src/webhook/handlers/initState.ts:244 +#: src/webhook/handlers/choosingArticle.ts:321 #, javascript-format msgid "I choose “${ displayTextWhenChosen }”" msgstr "「${displayTextWhenChosen}」を選ぶ" @@ -258,7 +260,7 @@ msgid "There is ${ otherReplyRequestCount } user also waiting for clarification. msgid_plural "There are ${ otherReplyRequestCount } users also waiting for clarification." msgstr[0] "計 ${ otherReplyRequestCount } 人がこの情報に関するリプライをチェックしたいと思っています。" -#: src/webhook/handlers/initState.ts:327 +#: src/webhook/handlers/initState.ts:184 #, javascript-format msgid "" "There are some messages that looks similar to \"${ inputSummary }\" you " @@ -269,34 +271,35 @@ msgstr "データベースにいくつかの情報は、送ってくれた「${ msgid "Please proceed on your mobile phone." msgstr "スマホで操作を続けてください。" -#: src/webhook/handlers/utils.ts:162 +#: src/webhook/handlers/askingCooccurrence.ts:125 +#: src/webhook/handlers/utils.ts:171 msgid "Be the first to report the message" msgstr "この情報にリプライする最初の人になりましょう" -#: src/webhook/handlers/utils.ts:150 -#, javascript-format +#: src/webhook/handlers/askingCooccurrence.ts:111 +#: src/webhook/handlers/utils.ts:159 msgid "press “${ btnText }” to make this message public on Cofacts database " msgstr "「${ btnText }」ボタンを押して Cofacts のデータベースで公開しましょう" -#: src/webhook/handlers/utils.ts:259 +#: src/webhook/handlers/utils.ts:268 msgid "Share on LINE" msgstr "LINE で聞く" -#: src/webhook/handlers/utils.ts:261 +#: src/webhook/handlers/utils.ts:270 #, javascript-format msgid "Please help me verify if this is true: ${ articleUrl }" msgstr "これは本当かどうか見てもらえませんか:${ articleUrl }" -#: src/webhook/handlers/utils.ts:271 +#: src/webhook/handlers/utils.ts:280 msgid "Share on Facebook" msgstr "Facebook で聞く" -#: src/webhook/handlers/utils.ts:275 +#: src/webhook/handlers/utils.ts:284 #. t: Facebook hash tag msgid "ReportedToCofacts" msgstr "Cofacts に問い合せる" -#: src/webhook/handlers/utils.ts:373 +#: src/webhook/handlers/utils.ts:382 msgid "" "We suggest forwarding the message to the following fact-checkers instead. " "They have 💁 1-on-1 Q&A service to respond to your questions." @@ -448,10 +451,6 @@ msgid "" "for more details." msgstr "誰かがあなたのチェックした情報に新しいリプライを書き込みました!" -#: src/webhook/handlers/initState.ts:176 -msgid "(Words found in the hyperlink)" -msgstr "(URL で見つかった文字)" - #: src/liff/pages/UserSetting.svelte:65 msgid "Welcome to Cofacts!" msgstr "Cofacts を追加して頂き、ありがとうございます!" @@ -474,11 +473,11 @@ msgstr "今のところ設定のオプションはありません :)" msgid "You can configure Cofacts here to meet your need." msgstr "ここでニーズに応じて Cofacts の設定ができます。" -#: src/webhook/handlers/utils.ts:324 +#: src/webhook/handlers/utils.ts:333 msgid "Go to settings" msgstr "設定する" -#: src/webhook/handlers/utils.ts:300 +#: src/webhook/handlers/utils.ts:309 msgid "Receive updates" msgstr "通知をオンにする" @@ -609,7 +608,7 @@ msgid "" "database." msgstr "検査結果は見当たらなかった場合、この情報をデータベースに入れるかどうかについてあなたの許可をお尋ねします。" -#: src/webhook/handlers/utils.ts:650 +#: src/webhook/handlers/utils.ts:659 msgid "" "This content is provided by Cofact message reporting chatbot and " "crowd-sourced fact-checking community under CC BY-SA 4.0 license. Please " @@ -645,8 +644,8 @@ msgid "You can try:" msgstr "" #: src/webhook/handlers/askingArticleSource.ts:77 -#: src/webhook/handlers/choosingArticle.ts:454 -#: src/webhook/handlers/utils.ts:686 +#: src/webhook/handlers/choosingArticle.ts:446 +#: src/webhook/handlers/utils.ts:695 msgid "Provide more detail" msgstr "" @@ -679,54 +678,54 @@ msgid "I see. Don’t trust the message just yet!" msgstr "" #: src/webhook/handlers/askingArticleSource.ts:188 -#: src/webhook/handlers/choosingArticle.ts:122 -#: src/webhook/handlers/processMedia.ts:341 +#: src/webhook/handlers/choosingArticle.ts:116 +#: src/webhook/handlers/processMedia.ts:168 msgid "Do you want someone to fact-check this message?" msgstr "" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:76 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:79 msgid "The message has not been reported and won’t be fact-checked. Thanks anyway!" msgstr "" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:167 -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:177 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:209 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:219 msgid "" "The message has now been recorded at Cofacts for volunteers to fact-check. " "Thank you for submitting!" msgstr "" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:183 -#: src/webhook/handlers/choosingArticle.ts:442 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:225 +#: src/webhook/handlers/choosingArticle.ts:434 msgid "View reported message" msgstr "" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:144 -#: src/webhook/handlers/choosingArticle.ts:386 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:237 +#: src/webhook/handlers/choosingArticle.ts:378 msgid "In the meantime, you can:" msgstr "" -#: src/webhook/handlers/choosingArticle.ts:97 +#: src/webhook/handlers/choosingArticle.ts:101 #, javascript-format msgid "" "I am sorry you cannot find the information “${ inputSummary }” you are " "looking for. But I would still like to help." msgstr "" -#: src/webhook/handlers/choosingArticle.ts:99 -#: src/webhook/handlers/initState.ts:354 +#: src/webhook/handlers/choosingArticle.ts:103 +#: src/webhook/handlers/initState.ts:206 msgid "May I ask you a quick question?" msgstr "" -#: src/webhook/handlers/choosingArticle.ts:120 +#: src/webhook/handlers/choosingArticle.ts:114 msgid "I am sorry you cannot find the information you are looking for." msgstr "" -#: src/webhook/handlers/choosingArticle.ts:167 +#: src/webhook/handlers/choosingArticle.ts:159 msgid "Provided message is not found." msgstr "" -#: src/webhook/handlers/choosingArticle.ts:422 -#: src/webhook/handlers/choosingArticle.ts:435 +#: src/webhook/handlers/choosingArticle.ts:414 +#: src/webhook/handlers/choosingArticle.ts:427 msgid "" "This message has already published on Cofacts, and will soon be " "fact-checked by volunteers.\n" @@ -741,84 +740,88 @@ msgstr "" msgid "Hi i am cofacts chat bot" msgstr "" -#: src/webhook/handlers/initState.ts:352 +#: src/webhook/handlers/initState.ts:204 #, javascript-format msgid "" "Unfortunately, I currently don’t recognize “${ inputSummary }”, but I would " "still like to help." msgstr "" -#: src/webhook/handlers/processMedia.ts:314 +#: src/webhook/handlers/processMedia.ts:146 msgid "There are some messages that looks similar to the one you have sent to me." msgstr "" -#: src/webhook/handlers/processMedia.ts:339 +#: src/webhook/handlers/processMedia.ts:166 #. submit msgid "" "Unfortunately, I currently don’t recognize this message, but I would still " "like to help." msgstr "" -#: src/webhook/handlers/utils.ts:142 +#: src/webhook/handlers/askingCooccurrence.ts:103 +#: src/webhook/handlers/utils.ts:151 msgid "Report to database" msgstr "" -#: src/webhook/handlers/utils.ts:146 +#: src/webhook/handlers/utils.ts:155 msgid "" "Currently we don’t have this message in our database. If you think it is " "most likely a rumor, " msgstr "" -#: src/webhook/handlers/utils.ts:156 +#: src/webhook/handlers/askingCooccurrence.ts:117 +#: src/webhook/handlers/utils.ts:165 msgid "" "and have volunteers fact-check it. This way you can help the people who " "receive the same message in the future." msgstr "" -#: src/webhook/handlers/utils.ts:200 -#: src/webhook/handlers/utils.ts:202 +#: src/webhook/handlers/askingCooccurrence.ts:157 +#: src/webhook/handlers/askingCooccurrence.ts:159 +#: src/webhook/handlers/utils.ts:209 +#: src/webhook/handlers/utils.ts:211 msgid "Don’t report" msgstr "" -#: src/webhook/handlers/utils.ts:246 +#: src/webhook/handlers/utils.ts:255 msgid "" "We all get by with a little help from our friends 🌟 Share your question to " "friends, someone might be able to help!" msgstr "" -#: src/webhook/handlers/utils.ts:311 +#: src/webhook/handlers/utils.ts:320 msgid "" "You can turn on notifications if you want Cofacts to notify you when " "someone replies to this message." msgstr "" -#: src/webhook/handlers/utils.ts:625 +#: src/webhook/handlers/utils.ts:634 msgid "Thank you for sharing “${ inputSummary }”" msgstr "" -#: src/webhook/handlers/utils.ts:625 +#: src/webhook/handlers/utils.ts:634 msgid "I found that there are some disagreement to the message:" msgstr "" -#: src/webhook/handlers/utils.ts:699 +#: src/webhook/handlers/utils.ts:708 msgid "It would help fact checkers a lot if you provide more detail :)" msgstr "" -#: src/webhook/handlers/utils.ts:712 +#: src/webhook/handlers/utils.ts:721 msgid "Provide detail" msgstr "" -#: src/webhook/handlers/utils.ts:791 +#: src/webhook/handlers/utils.ts:800 msgid "Did you forward this message as a whole to me from the LINE app?" msgstr "" -#: src/webhook/handlers/utils.ts:817 -#: src/webhook/handlers/utils.ts:819 +#: src/webhook/handlers/utils.ts:826 +#: src/webhook/handlers/utils.ts:828 msgid "Yes, I forwarded it as a whole" msgstr "" -#: src/webhook/handlers/utils.ts:829 -#: src/webhook/handlers/utils.ts:831 +#: src/webhook/handlers/utils.ts:838 +#: src/webhook/handlers/utils.ts:840 msgid "No, typed it myself" msgstr "" @@ -1023,34 +1026,105 @@ msgstr "" msgid "${ dateStr } ago" msgstr "" -#: src/webhook/handlers/initState.ts:180 -msgid "(Words found in transcript)" -msgstr "" - -#: src/webhook/handlers/processMedia.ts:142 +#: src/webhook/handlers/utils.ts:1025 msgid "Similar file" msgstr "" -#: src/webhook/handlers/processMedia.ts:143 +#: src/webhook/handlers/utils.ts:1026 msgid "Contains relevant text" msgstr "" -#: src/webhook/handlers/processMedia.ts:151 +#: src/webhook/handlers/utils.ts:1035 msgid "(Text in the hyperlink)" msgstr "" -#: src/webhook/handlers/processMedia.ts:155 +#: src/webhook/handlers/utils.ts:1039 msgid "(Text in transcript)" msgstr "" -#: src/webhook/handlers/processMedia.ts:230 +#: src/webhook/handlers/utils.ts:1115 #, javascript-format msgid "I choose ${ displayTextWhenChosen }" msgstr "" -#: src/webhook/handlers/utils.ts:558 +#: src/webhook/handlers/askingCooccurrence.ts:181 +#. Get first few search results for each message, and make at most 10 options +#. +msgid "There are some messages that looks similar to the ones you have sent to me." +msgstr "" + +#: src/webhook/handlers/askingCooccurrence.ts:57 +msgid "Please send me the messages separately." +msgstr "" + +#: src/webhook/handlers/askingCooccurrence.ts:96 +#, javascript-format +msgid "" +"None of the ${ notInDbMsgIndexes.length } messages you sent are in the " +"Cofacts database." +msgstr "" + +#: src/webhook/handlers/askingCooccurrence.ts:97 +#, javascript-format +msgid "" +"Out of the ${ totalCount } messages you sent, ${ notInDbMsgIndexes.length } " +"is not in the Cofacts database." +msgid_plural "" +"Out of the ${ totalCount } messages you sent, ${ notInDbMsgIndexes.length } " +"are not in the Cofacts database." +msgstr[0] "" +msgstr[1] "" + +#: src/webhook/handlers/askingCooccurrence.ts:107 +#, javascript-format +msgid "${ inDbStatus } If you think they are most likely a rumor," +msgstr "" + +#: src/webhook/handlers/processBatch.ts:24 +#, javascript-format +msgid "" +"May I ask if the ${ msgCount } messages above were sent by the same person " +"at the same time?" +msgstr "" + +#: src/webhook/handlers/processBatch.ts:33 +msgid "Yes, same person at same time" +msgstr "" + +#: src/webhook/handlers/processBatch.ts:43 +msgid "No, from different person or at different time" +msgstr "" + +#: src/webhook/handlers/utils.ts:137 +msgid "replied at" +msgstr "" + +#: src/webhook/handlers/utils.ts:567 #, javascript-format msgid "" "Someone on the internet replies to the message first reported on ${ " "articleDate }:" +msgstr "" + +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:242 +msgid "" +"This article is still under verification, please refrain from believing it " +"for now. \n" +"Below is the preliminary analysis result by the bot, hoping to provide you " +"with some insights." +msgstr "" + +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:246 +msgid "After reading the automatic analysis by the bot above, you can:" +msgstr "" + +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:168 +msgid "" +"Thank you for submitting! Now the messages has been recorded in the Cofacts " +"database." +msgstr "" + +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:171 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:175 +msgid "Please choose the messages you would like to view" msgstr "" \ No newline at end of file diff --git a/i18n/zh_TW.po b/i18n/zh_TW.po index 54e01a85..3e6c9992 100644 --- a/i18n/zh_TW.po +++ b/i18n/zh_TW.po @@ -6,11 +6,11 @@ msgstr "" "mime-version: 1.0\n" "Content-Transfer-Encoding: 8bit\n" -#: src/webhook/handlers/utils.ts:85 +#: src/webhook/handlers/utils.ts:94 msgid "No feedback yet" msgstr "還沒有人針對此回應評價" -#: src/webhook/handlers/utils.ts:99 +#: src/webhook/handlers/utils.ts:108 msgid "${ negative } user consider this not useful" msgid_plural "${ negative } users consider this not useful" msgstr[0] "有 ${negative} 人覺得此回應沒幫助" @@ -31,47 +31,45 @@ msgstr "含有個人意見" msgid "Invalid request" msgstr "不在查證範圍" -#: src/webhook/handlers/utils.ts:126 +#: src/webhook/handlers/utils.ts:135 msgid "different opinions" msgstr "不同觀點" -#: src/webhook/handlers/utils.ts:126 +#: src/webhook/handlers/utils.ts:135 msgid "references" msgstr "出處" -#: src/webhook/handlers/utils.ts:128 -msgid "replied at" -msgstr "回應日期" - -#: src/webhook/handlers/utils.ts:132 +#: src/webhook/handlers/utils.ts:141 #, javascript-format msgid "This reply has no ${ prompt } and it may be biased" msgstr "此回應沒有${prompt},請自行斟酌回應之可信度。" -#: src/webhook/handlers/choosingArticle.ts:224 +#: src/webhook/handlers/choosingArticle.ts:216 msgid "Volunteer editors have published several replies to this message." msgstr "真的假的查證志工對這則訊息發表了多則看法唷!" -#: src/webhook/handlers/choosingArticle.ts:350 +#: src/webhook/handlers/choosingArticle.ts:342 msgid "Let's pick one" msgstr "選一則來閱讀吧" -#: src/webhook/handlers/choosingArticle.ts:327 +#: src/webhook/handlers/choosingArticle.ts:319 msgid "Take a look" msgstr "看他怎麼說" -#: src/webhook/handlers/choosingArticle.ts:366 +#: src/webhook/handlers/choosingArticle.ts:358 #, javascript-format msgid "Visit ${ articleUrl } for more replies." msgstr "更多回應請到:${ articleUrl }" -#: src/webhook/handlers/initState.ts:242 -#: src/webhook/handlers/processMedia.ts:228 +#: src/webhook/handlers/utils.ts:1113 msgid "Choose this one" msgstr "選擇這篇" -#: src/webhook/handlers/initState.ts:334 -#: src/webhook/handlers/processMedia.ts:321 +#: src/webhook/handlers/askingCooccurrence.ts:185 +#: src/webhook/handlers/initState.ts:188 +#: src/webhook/handlers/processMedia.ts:150 +#. Get first few search results for each message, and make at most 10 options +#. msgid "" "Internet rumors are often mutated and shared.\n" "Please choose the version that looks the most similar" @@ -79,18 +77,18 @@ msgstr "" "不實訊息常常會被人修改重發。\n" "請選擇比較接近的版本" -#: src/webhook/handlers/utils.ts:649 +#: src/webhook/handlers/utils.ts:658 #, javascript-format msgid "Therefore, the author think the message ${ typeStr }." msgstr "綜合以上,回應者認為它${typeStr}。" -#: src/webhook/handlers/utils.ts:652 +#: src/webhook/handlers/utils.ts:661 msgid "" "There are different replies for the message. Read them all here before " "making judgements:" msgstr "這則訊息有很多不同回應,建議到這裡一次讀完再下判斷:" -#: src/webhook/handlers/utils.ts:654 +#: src/webhook/handlers/utils.ts:663 msgid "If you have different thoughts, you may have your say here:" msgstr "如果你對這則訊息有不同看法,歡迎到下面這裡寫入新的回應:" @@ -102,70 +100,74 @@ msgstr "請問上面回應是否有幫助?" #: src/liff/components/FeedbackForm.svelte:27 #: src/webhook/handlers/choosingReply.ts:63 +#: src/webhook/handlers/processBatch.ts:31 msgid "Yes" msgstr "是" #: src/liff/components/FeedbackForm.svelte:30 #: src/webhook/handlers/choosingReply.ts:73 +#: src/webhook/handlers/processBatch.ts:41 msgid "No" msgstr "否" -#: src/webhook/handlers/initState.ts:285 -#: src/webhook/handlers/initState.ts:303 -#: src/webhook/handlers/processMedia.ts:272 -#: src/webhook/handlers/processMedia.ts:290 +#: src/webhook/handlers/initState.ts:143 +#: src/webhook/handlers/initState.ts:161 +#: src/webhook/handlers/processMedia.ts:105 +#: src/webhook/handlers/processMedia.ts:123 msgid "None of these messages matches mine :(" msgstr "找不到我想查的訊息 QQ" -#: src/webhook/handlers/utils.ts:90 +#: src/webhook/handlers/utils.ts:99 #, javascript-format msgid "${ positive } user considers this helpful" msgid_plural "${ positive } users consider this helpful" msgstr[0] "有 ${positive} 人覺得此回應有幫助" -#: src/webhook/handlers/initState.ts:301 -#: src/webhook/handlers/processMedia.ts:288 +#: src/webhook/handlers/initState.ts:159 +#: src/webhook/handlers/processMedia.ts:121 msgid "Tell us more" msgstr "回報此訊息" -#: src/webhook/handlers/initState.ts:317 -#: src/webhook/handlers/processMedia.ts:304 +#: src/webhook/handlers/askingCooccurrence.ts:190 +#: src/webhook/handlers/initState.ts:175 +#: src/webhook/handlers/processMedia.ts:137 +#. Get first few search results for each message, and make at most 10 options +#. msgid "Please choose the most similar message from the list." msgstr "請從下列選擇您要查的訊息。" -#: src/webhook/handlers/choosingArticle.ts:354 +#: src/webhook/handlers/choosingArticle.ts:346 msgid "Please take a look at the following replies." msgstr "請從下列選擇您要查看的回應。" -#: src/webhook/handlers/choosingArticle.ts:273 +#: src/webhook/handlers/choosingArticle.ts:265 #, javascript-format msgid "Someone thinks it ${ typeWords }" msgstr "有人認為它${typeWords}" -#: src/webhook/handlers/initState.ts:219 -#: src/webhook/handlers/processMedia.ts:140 +#: src/webhook/handlers/utils.ts:1023 #, javascript-format msgid "Looks ${ similarityPercentage }% similar" msgstr "看起來 ${similarityPercentage}% 像" -#: src/webhook/handlers/singleUserHandler.ts:77 +#: src/webhook/handlers/singleUserHandler.ts:82 msgid "" "Line bot is busy, or we cannot handle this message. Maybe you can try again " "a few minutes later." msgstr "不好意思!系統可能在忙線中,無法及時處理您傳的訊息。請稍等幾分鐘再試試看唷。" -#: src/webhook/handlers/handlePostback.ts:81 +#: src/webhook/handlers/handlePostback.ts:86 msgid "Wrong usage" msgstr "這不是這樣用的" -#: src/webhook/handlers/singleUserHandler.ts:272 +#: src/webhook/handlers/singleUserHandler.ts:283 #. Reuse existing context msgid "" "You are currently searching for another message, buttons from previous " "search sessions do not work now." msgstr "您已經在搜尋新的訊息了,過去查過的訊息的按鈕已經失效囉。" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:137 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:192 #, javascript-format msgid "Your submission is now recorded at ${ articleUrl }" msgstr "您回報的訊息已經被收錄至 ${ articleUrl }" @@ -206,34 +208,34 @@ msgid "Share to friends" msgstr "分享給朋友" #: src/webhook/handlers/askingArticleSource.ts:34 -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:50 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:53 +#: src/webhook/handlers/askingCooccurrence.ts:39 #: src/webhook/handlers/choosingArticle.ts:71 #: src/webhook/handlers/choosingReply.ts:152 msgid "Please choose from provided options." msgstr "請從上面的選項中做選擇。" -#: src/webhook/handlers/choosingArticle.ts:228 +#: src/webhook/handlers/choosingArticle.ts:220 #, javascript-format msgid "${ countOfType.RUMOR } of them say it ❌ contains misinformation." msgstr "${ countOfType.RUMOR } 人認為它 ❌ 含有不實訊息。" -#: src/webhook/handlers/choosingArticle.ts:231 +#: src/webhook/handlers/choosingArticle.ts:223 msgid "${ countOfType.NOT_RUMOR } of them says it ⭕ contains true information." msgstr "${ countOfType.NOT_RUMOR } 人認為它 ⭕ 含有真實訊息。" -#: src/webhook/handlers/choosingArticle.ts:234 +#: src/webhook/handlers/choosingArticle.ts:226 #, javascript-format msgid "" "${ countOfType.OPINIONATED } of them says it 💬 contains personal " "perspective." msgstr "${ countOfType.OPINIONATED } 人認為它 💬 含有個人意見。" -#: src/webhook/handlers/choosingArticle.ts:237 +#: src/webhook/handlers/choosingArticle.ts:229 msgid "${ countOfType.NOT_ARTICLE } of them says it ⚠️️ is out of scope of Cofacts." msgstr "${ countOfType.NOT_ARTICLE } 人認為它 ⚠️️ 不在 Cofacts 的查證範圍。" -#: src/webhook/handlers/choosingArticle.ts:329 -#: src/webhook/handlers/initState.ts:244 +#: src/webhook/handlers/choosingArticle.ts:321 #, javascript-format msgid "I choose “${ displayTextWhenChosen }”" msgstr "我要選「${displayTextWhenChosen}」" @@ -258,7 +260,7 @@ msgid "There is ${ otherReplyRequestCount } user also waiting for clarification. msgid_plural "There are ${ otherReplyRequestCount } users also waiting for clarification." msgstr[0] "另有 ${ otherReplyRequestCount } 人跟您一樣渴望看到針對這篇訊息的回應。" -#: src/webhook/handlers/initState.ts:327 +#: src/webhook/handlers/initState.ts:184 #, javascript-format msgid "" "There are some messages that looks similar to \"${ inputSummary }\" you " @@ -269,34 +271,35 @@ msgstr "資料庫裡有幾篇訊息,跟您傳給我的「${ inputSummary }」 msgid "Please proceed on your mobile phone." msgstr "請在您的手機上繼續操作。" -#: src/webhook/handlers/utils.ts:162 +#: src/webhook/handlers/askingCooccurrence.ts:125 +#: src/webhook/handlers/utils.ts:171 msgid "Be the first to report the message" msgstr "成為全球首位回報此訊息的人" -#: src/webhook/handlers/utils.ts:150 -#, javascript-format +#: src/webhook/handlers/askingCooccurrence.ts:111 +#: src/webhook/handlers/utils.ts:159 msgid "press “${ btnText }” to make this message public on Cofacts database " msgstr "請按「${ btnText }」在 Cofacts 真的假的資料庫公開這則訊息" -#: src/webhook/handlers/utils.ts:259 +#: src/webhook/handlers/utils.ts:268 msgid "Share on LINE" msgstr "在 LINE 上問人" -#: src/webhook/handlers/utils.ts:261 +#: src/webhook/handlers/utils.ts:270 #, javascript-format msgid "Please help me verify if this is true: ${ articleUrl }" msgstr "請幫我看看這是真的還是假的:${ articleUrl }" -#: src/webhook/handlers/utils.ts:271 +#: src/webhook/handlers/utils.ts:280 msgid "Share on Facebook" msgstr "請教臉書大神" -#: src/webhook/handlers/utils.ts:275 -#. t: Facebook hash tag +#: src/webhook/handlers/utils.ts:284 +#. t: Facebook hash tag msgid "ReportedToCofacts" msgstr "Cofacts求解惑" -#: src/webhook/handlers/utils.ts:373 +#: src/webhook/handlers/utils.ts:382 msgid "" "We suggest forwarding the message to the following fact-checkers instead. " "They have 💁 1-on-1 Q&A service to respond to your questions." @@ -316,7 +319,7 @@ msgid "Do you have anything to add about the reply?" msgstr "針對這則回應,有沒有想補充的呢?" #: src/liff/components/ReplyRequestForm.svelte:14 -#. t: Guidance in LIFF +#. t: Guidance in LIFF msgid "" "You may try:\n" "1. Express your thought more\n" @@ -329,7 +332,7 @@ msgstr "" "3. 把全文複製貼上到 Facebook 搜尋框看看" #: src/liff/components/ReplyRequestForm.svelte:20 -#. t: Guidance in LIFF +#. t: Guidance in LIFF #, javascript-format msgid "" "It would help fact-checking editors a lot if you provide more info :)\n" @@ -347,7 +350,7 @@ msgstr "" "若要補充資訊,請按「取消」;覺得現在這樣送出就好,請按「確定」。" #: src/liff/components/ReplyRequestForm.svelte:27 -#. t: Guidance in LIFF +#. t: Guidance in LIFF msgid "" "The info you provide should not be identical to the message itself.\n" "\n" @@ -446,10 +449,6 @@ msgid "" "for more details." msgstr "有人針對您之前曾看過的訊息,寫了新的查證回應唷!" -#: src/webhook/handlers/initState.ts:176 -msgid "(Words found in the hyperlink)" -msgstr "(網址裡找到的字)" - #: src/liff/pages/UserSetting.svelte:65 msgid "Welcome to Cofacts!" msgstr "感謝您加 Cofacts 好友!" @@ -472,11 +471,11 @@ msgstr "目前沒有設定選項 :)" msgid "You can configure Cofacts here to meet your need." msgstr "您可以在這裡設定 Cofacts 以符合需求。" -#: src/webhook/handlers/utils.ts:324 +#: src/webhook/handlers/utils.ts:333 msgid "Go to settings" msgstr "前往設定" -#: src/webhook/handlers/utils.ts:300 +#: src/webhook/handlers/utils.ts:309 msgid "Receive updates" msgstr "開啟小鈴鐺" @@ -607,7 +606,7 @@ msgid "" "database." msgstr "如果我找不到的話,會徵求你的同意,看看要不要把這個訊息送進資料庫唷。" -#: src/webhook/handlers/utils.ts:650 +#: src/webhook/handlers/utils.ts:659 msgid "" "This content is provided by Cofact message reporting chatbot and " "crowd-sourced fact-checking community under CC BY-SA 4.0 license. Please " @@ -616,7 +615,7 @@ msgstr "" "此內容由「Cofacts 真的假的」訊息回報機器人與查核協作社群提供,以創用 CC 姓名標示-相同方式分享 4.0 國際 " "授權條款釋出。請斟酌出處與理由,自行思考判斷。" -#: src/webhook/handlers/utils.ts:625 +#: src/webhook/handlers/utils.ts:634 msgid "I found that there are some disagreement to the message:" msgstr "剛剛這則訊息,我查過似乎發現有點問題。有另外一種說法是:" @@ -624,11 +623,11 @@ msgstr "剛剛這則訊息,我查過似乎發現有點問題。有另外一種 msgid "Hi i am cofacts chat bot" msgstr "Hi 我是「Cofacts 真的假的」訊息查證機器人~" -#: src/webhook/handlers/utils.ts:625 +#: src/webhook/handlers/utils.ts:634 msgid "Thank you for sharing “${ inputSummary }”" msgstr "感謝您的分享 “${ inputSummary }”" -#: src/webhook/handlers/choosingArticle.ts:167 +#: src/webhook/handlers/choosingArticle.ts:159 msgid "Provided message is not found." msgstr "找不到此訊息。" @@ -817,27 +816,27 @@ msgstr "此訊息不存在。" msgid "The reply does not exist. Maybe it has been deleted by its author." msgstr "此回應不存在。或許已經被作者刪掉囉。" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:183 -#: src/webhook/handlers/choosingArticle.ts:442 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:225 +#: src/webhook/handlers/choosingArticle.ts:434 msgid "View reported message" msgstr "檢視回報訊息" #: src/webhook/handlers/askingArticleSource.ts:77 -#: src/webhook/handlers/choosingArticle.ts:454 -#: src/webhook/handlers/utils.ts:686 +#: src/webhook/handlers/choosingArticle.ts:446 +#: src/webhook/handlers/utils.ts:695 msgid "Provide more detail" msgstr "提供更多情報" -#: src/webhook/handlers/utils.ts:699 +#: src/webhook/handlers/utils.ts:708 msgid "It would help fact checkers a lot if you provide more detail :)" msgstr "您可以提供更多關於此訊息的情報給查證志工,讓好心人更容易查真假唷!" -#: src/webhook/handlers/utils.ts:712 +#: src/webhook/handlers/utils.ts:721 msgid "Provide detail" msgstr "提供更多情報" -#: src/webhook/handlers/choosingArticle.ts:422 -#: src/webhook/handlers/choosingArticle.ts:435 +#: src/webhook/handlers/choosingArticle.ts:414 +#: src/webhook/handlers/choosingArticle.ts:427 msgid "" "This message has already published on Cofacts, and will soon be " "fact-checked by volunteers.\n" @@ -846,18 +845,18 @@ msgstr "" "此訊息已經被收錄至 Cofacts 有待好心人來查證。\n" "請先不要相信這個訊息唷!" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:144 -#: src/webhook/handlers/choosingArticle.ts:386 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:237 +#: src/webhook/handlers/choosingArticle.ts:378 msgid "In the meantime, you can:" msgstr "接下來您可以:" -#: src/webhook/handlers/utils.ts:246 +#: src/webhook/handlers/utils.ts:255 msgid "" "We all get by with a little help from our friends 🌟 Share your question to " "friends, someone might be able to help!" msgstr "遠親不如近鄰🌟問問親友總沒錯。把訊息分享給朋友們,說不定有人能幫你解惑!" -#: src/webhook/handlers/utils.ts:311 +#: src/webhook/handlers/utils.ts:320 msgid "" "You can turn on notifications if you want Cofacts to notify you when " "someone replies to this message." @@ -868,19 +867,19 @@ msgid "Instructions" msgstr "指示" #: src/webhook/handlers/askingArticleSource.ts:60 -#. t: ~ entire message that ... +#. t: ~ entire message that ... msgid "" "I am a bot which only recognizes messages forwarded on LINE, therefore it " "is important to send me the" msgstr "我是一個機器人,只認得在 LINE 上面傳來傳去的訊息,所以把" #: src/webhook/handlers/askingArticleSource.ts:64 -#. t: emphasized text in sentence "It is important to send me the ~ that is being passed around" +#. t: emphasized text in sentence "It is important to send me the ~ that is being passed around" msgid " entire message " msgstr "整篇訊息" #: src/webhook/handlers/askingArticleSource.ts:70 -#. t: the entire message ~ +#. t: the entire message ~ msgid "that is being passed around so I can identify it." msgstr "給我,我才能辨認唷!" @@ -917,73 +916,77 @@ msgid "I see. Don’t trust the message just yet!" msgstr "這樣呀。請先不要相信這個訊息唷!" #: src/webhook/handlers/askingArticleSource.ts:188 -#: src/webhook/handlers/choosingArticle.ts:122 -#: src/webhook/handlers/processMedia.ts:341 +#: src/webhook/handlers/choosingArticle.ts:116 +#: src/webhook/handlers/processMedia.ts:168 msgid "Do you want someone to fact-check this message?" msgstr "你要請人查查這則訊息嗎?" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:76 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:79 msgid "The message has not been reported and won’t be fact-checked. Thanks anyway!" msgstr "好的,那就不查囉。還是謝謝你!" -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:167 -#: src/webhook/handlers/askingArticleSubmissionConsent.ts:177 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:209 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:219 msgid "" "The message has now been recorded at Cofacts for volunteers to fact-check. " "Thank you for submitting!" msgstr "訊息已經被收錄至 Cofacts 真的假的,讓好心人來查證。謝謝您的回報!" -#: src/webhook/handlers/choosingArticle.ts:97 +#: src/webhook/handlers/choosingArticle.ts:101 #, javascript-format msgid "" "I am sorry you cannot find the information “${ inputSummary }” you are " "looking for. But I would still like to help." msgstr "抱歉沒有找到你想要查詢的「${ inputSummary }」。可是我還是想幫你查證。" -#: src/webhook/handlers/initState.ts:352 +#: src/webhook/handlers/initState.ts:204 #, javascript-format msgid "" "Unfortunately, I currently don’t recognize “${ inputSummary }”, but I would " "still like to help." msgstr "抱歉我目前還不認得「${ inputSummary }」這個訊息,可是我還是想幫你查證。" -#: src/webhook/handlers/utils.ts:142 +#: src/webhook/handlers/askingCooccurrence.ts:103 +#: src/webhook/handlers/utils.ts:151 msgid "Report to database" msgstr "送進資料庫查核" -#: src/webhook/handlers/utils.ts:146 +#: src/webhook/handlers/utils.ts:155 msgid "" "Currently we don’t have this message in our database. If you think it is " "most likely a rumor, " msgstr "目前資料庫裡沒有您傳的訊息。若您覺得它很可能是一則謠言," -#: src/webhook/handlers/utils.ts:156 +#: src/webhook/handlers/askingCooccurrence.ts:117 +#: src/webhook/handlers/utils.ts:165 msgid "" "and have volunteers fact-check it. This way you can help the people who " "receive the same message in the future." msgstr "、讓好心人查證與回覆。您可以幫助到未來同樣收到這份訊息的人。" -#: src/webhook/handlers/utils.ts:200 -#: src/webhook/handlers/utils.ts:202 +#: src/webhook/handlers/askingCooccurrence.ts:157 +#: src/webhook/handlers/askingCooccurrence.ts:159 +#: src/webhook/handlers/utils.ts:209 +#: src/webhook/handlers/utils.ts:211 msgid "Don’t report" msgstr "我不想回報訊息" -#: src/webhook/handlers/utils.ts:791 +#: src/webhook/handlers/utils.ts:800 msgid "Did you forward this message as a whole to me from the LINE app?" msgstr "請問這個訊息是你從 LINE 裡整篇分享給我的嗎?" -#: src/webhook/handlers/utils.ts:817 -#: src/webhook/handlers/utils.ts:819 +#: src/webhook/handlers/utils.ts:826 +#: src/webhook/handlers/utils.ts:828 msgid "Yes, I forwarded it as a whole" msgstr "是,我整篇分享過來的" -#: src/webhook/handlers/utils.ts:829 -#: src/webhook/handlers/utils.ts:831 +#: src/webhook/handlers/utils.ts:838 +#: src/webhook/handlers/utils.ts:840 msgid "No, typed it myself" msgstr "否,我自行打字輸入的" -#: src/webhook/handlers/choosingArticle.ts:99 -#: src/webhook/handlers/initState.ts:354 +#: src/webhook/handlers/choosingArticle.ts:103 +#: src/webhook/handlers/initState.ts:206 msgid "May I ask you a quick question?" msgstr "想先請教您一個問題:" @@ -991,15 +994,15 @@ msgstr "想先請教您一個問題:" msgid "Provide better reply" msgstr "提供更好的回應" -#: src/webhook/handlers/processMedia.ts:314 +#: src/webhook/handlers/processMedia.ts:146 msgid "There are some messages that looks similar to the one you have sent to me." msgstr "資料庫裡有幾篇訊息,跟您傳給我的有些接近。" -#: src/webhook/handlers/choosingArticle.ts:120 +#: src/webhook/handlers/choosingArticle.ts:114 msgid "I am sorry you cannot find the information you are looking for." msgstr "抱歉沒有找到你想要查詢的訊息。" -#: src/webhook/handlers/processMedia.ts:339 +#: src/webhook/handlers/processMedia.ts:166 #. submit msgid "" "Unfortunately, I currently don’t recognize this message, but I would still " @@ -1023,34 +1026,106 @@ msgstr "尚未支援預覽" msgid "An audio" msgstr "語音訊息一則" -#: src/webhook/handlers/initState.ts:180 -msgid "(Words found in transcript)" -msgstr "(逐字稿內的字)" - -#: src/webhook/handlers/processMedia.ts:142 +#: src/webhook/handlers/utils.ts:1025 msgid "Similar file" msgstr "相似檔案" -#: src/webhook/handlers/processMedia.ts:143 +#: src/webhook/handlers/utils.ts:1026 msgid "Contains relevant text" msgstr "含有相關文字" -#: src/webhook/handlers/processMedia.ts:151 +#: src/webhook/handlers/utils.ts:1035 msgid "(Text in the hyperlink)" msgstr "網址內的字" -#: src/webhook/handlers/processMedia.ts:155 +#: src/webhook/handlers/utils.ts:1039 msgid "(Text in transcript)" msgstr "逐字稿內的字" -#: src/webhook/handlers/processMedia.ts:230 +#: src/webhook/handlers/utils.ts:1115 #, javascript-format msgid "I choose ${ displayTextWhenChosen }" msgstr "我選 ${ displayTextWhenChosen }" -#: src/webhook/handlers/utils.ts:558 +#: src/webhook/handlers/askingCooccurrence.ts:181 +#. Get first few search results for each message, and make at most 10 options +#. +msgid "There are some messages that looks similar to the ones you have sent to me." +msgstr "資料庫裡有幾篇訊息,跟您傳給我的有些接近。" + +#: src/webhook/handlers/askingCooccurrence.ts:57 +msgid "Please send me the messages separately." +msgstr "好的,那再請您將這些訊息一則一則分開傳給我唷。" + +#: src/webhook/handlers/askingCooccurrence.ts:96 +#, javascript-format +msgid "" +"None of the ${ notInDbMsgIndexes.length } messages you sent are in the " +"Cofacts database." +msgstr "您傳的 ${ notInDbMsgIndexes.length } 則訊息,目前都不在 Cofacts 資料庫裡。" + +#: src/webhook/handlers/askingCooccurrence.ts:97 +#, javascript-format +msgid "" +"Out of the ${ totalCount } messages you sent, ${ notInDbMsgIndexes.length } " +"is not in the Cofacts database." +msgid_plural "" +"Out of the ${ totalCount } messages you sent, ${ notInDbMsgIndexes.length } " +"are not in the Cofacts database." +msgstr[0] "在您傳的 ${ totalCount } 則訊息中,有 ${ notInDbMsgIndexes.length } 則不在 Cofacts 資料庫裡。" + +#: src/webhook/handlers/askingCooccurrence.ts:107 +#, javascript-format +msgid "${ inDbStatus } If you think they are most likely a rumor," +msgstr "${ inDbStatus }若您覺得它們很可能是謠言," + +#: src/webhook/handlers/processBatch.ts:24 +#, javascript-format +msgid "" +"May I ask if the ${ msgCount } messages above were sent by the same person " +"at the same time?" +msgstr "請問這 ${ msgCount } 則訊息,是由同一個人、同時傳送的嗎?" + +#: src/webhook/handlers/processBatch.ts:33 +msgid "Yes, same person at same time" +msgstr "是同一人、同時傳送的" + +#: src/webhook/handlers/processBatch.ts:43 +msgid "No, from different person or at different time" +msgstr "是由不同人、或不同時間傳的" + +#: src/webhook/handlers/utils.ts:137 +msgid "replied at" +msgstr "回應日期" + +#: src/webhook/handlers/utils.ts:567 #, javascript-format msgid "" "Someone on the internet replies to the message first reported on ${ " "articleDate }:" -msgstr "網路上有人這樣回應這則 ${articleDate} 回報的訊息:" \ No newline at end of file +msgstr "網路上有人這樣回應這則 ${articleDate} 回報的訊息:" + +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:242 +msgid "" +"This article is still under verification, please refrain from believing it " +"for now. \n" +"Below is the preliminary analysis result by the bot, hoping to provide you " +"with some insights." +msgstr "" +"這篇文章尚待查核中,請先不要相信這篇文章。\n" +"以下是機器人初步分析此篇訊息的結果,希望能帶給你一些想法。" + +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:246 +msgid "After reading the automatic analysis by the bot above, you can:" +msgstr "讀完以上機器人的自動分析後,您可以:" + +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:168 +msgid "" +"Thank you for submitting! Now the messages has been recorded in the Cofacts " +"database." +msgstr "感謝提供!現在資料庫裡有這些訊息囉。" + +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:171 +#: src/webhook/handlers/askingArticleSubmissionConsent.ts:175 +msgid "Please choose the messages you would like to view" +msgstr "請選擇要查看的訊息" \ No newline at end of file diff --git a/src/types/chatbotState.ts b/src/types/chatbotState.ts index 84db9b1c..36ff157a 100644 --- a/src/types/chatbotState.ts +++ b/src/types/chatbotState.ts @@ -7,6 +7,7 @@ export type ChatbotState = | 'CHOOSING_REPLY' | 'ASKING_ARTICLE_SOURCE' | 'ASKING_ARTICLE_SUBMISSION_CONSENT' + | 'ASKING_COOCCURRENCE' | 'Error'; export type LegacyContext = { diff --git a/src/webhook/handlers/__tests__/__snapshots__/askingArticleSubmissionConsent.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/askingArticleSubmissionConsent.test.ts.snap index f302930a..7cd919a5 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/askingArticleSubmissionConsent.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/askingArticleSubmissionConsent.test.ts.snap @@ -186,14 +186,12 @@ Object { "type": "flex", }, Object { - "altText": "這篇文章尚待查核中,請先不要相信這篇文章。 -以下是機器人初步分析此篇訊息的結果,希望能帶給你一些想法。", + "altText": "This article is still under verification, please refrain from believing it for now. \\\\nBelow is the preliminary analysis result by the bot, hoping to provide you with some insights.", "contents": Object { "body": Object { "contents": Array [ Object { - "text": "這篇文章尚待查核中,請先不要相信這篇文章。 -以下是機器人初步分析此篇訊息的結果,希望能帶給你一些想法。", + "text": "This article is still under verification, please refrain from believing it for now. \\\\nBelow is the preliminary analysis result by the bot, hoping to provide you with some insights.", "type": "text", "wrap": true, }, @@ -215,12 +213,12 @@ Object { "type": "text", }, Object { - "altText": "讀完以上機器人的自動分析後,您可以:", + "altText": "After reading the automatic analysis by the bot above, you can:", "contents": Object { "body": Object { "contents": Array [ Object { - "text": "讀完以上機器人的自動分析後,您可以:", + "text": "After reading the automatic analysis by the bot above, you can:", "type": "text", "wrap": true, }, diff --git a/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap b/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap index 7e5b79bf..629a3d95 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/choosingArticle.test.ts.snap @@ -156,7 +156,7 @@ Do you want someone to fact-check this message?", "contents": Array [ Object { "action": Object { - "data": "{\\"input\\":\\"__POSTBACK_YES__\\",\\"sessionId\\":0,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", + "data": "{\\"input\\":[0],\\"sessionId\\":0,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", "displayText": "🆕 Report to database", "label": "🆕 Report to database", "type": "postback", @@ -167,7 +167,7 @@ Do you want someone to fact-check this message?", }, Object { "action": Object { - "data": "{\\"input\\":\\"__POSTBACK_NO__\\",\\"sessionId\\":0,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", + "data": "{\\"input\\":[],\\"sessionId\\":0,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", "displayText": "Don’t report", "label": "Don’t report", "type": "postback", diff --git a/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap b/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap index b670fd23..d50d7ad3 100644 --- a/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap +++ b/src/webhook/handlers/__tests__/__snapshots__/processMedia.test.js.snap @@ -591,7 +591,7 @@ Do you want someone to fact-check this message?", "contents": Array [ Object { "action": Object { - "data": "{\\"input\\":\\"__POSTBACK_YES__\\",\\"sessionId\\":1577836800000,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", + "data": "{\\"input\\":[0],\\"sessionId\\":1577836800000,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", "displayText": "🆕 Report to database", "label": "🆕 Report to database", "type": "postback", @@ -602,7 +602,7 @@ Do you want someone to fact-check this message?", }, Object { "action": Object { - "data": "{\\"input\\":\\"__POSTBACK_NO__\\",\\"sessionId\\":1577836800000,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", + "data": "{\\"input\\":[],\\"sessionId\\":1577836800000,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", "displayText": "Don’t report", "label": "Don’t report", "type": "postback", diff --git a/src/webhook/handlers/__tests__/askingArticleSource.test.ts b/src/webhook/handlers/__tests__/askingArticleSource.test.ts index 8917d7fb..7000acc3 100644 --- a/src/webhook/handlers/__tests__/askingArticleSource.test.ts +++ b/src/webhook/handlers/__tests__/askingArticleSource.test.ts @@ -295,7 +295,7 @@ it('sends user submission consent if user forwarded the whole message', async () "contents": Array [ Object { "action": Object { - "data": "{\\"input\\":\\"__POSTBACK_YES__\\",\\"sessionId\\":0,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", + "data": "{\\"input\\":[0],\\"sessionId\\":0,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", "displayText": "🆕 Report to database", "label": "🆕 Report to database", "type": "postback", @@ -306,7 +306,7 @@ it('sends user submission consent if user forwarded the whole message', async () }, Object { "action": Object { - "data": "{\\"input\\":\\"__POSTBACK_NO__\\",\\"sessionId\\":0,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", + "data": "{\\"input\\":[],\\"sessionId\\":0,\\"state\\":\\"ASKING_ARTICLE_SUBMISSION_CONSENT\\"}", "displayText": "Don’t report", "label": "Don’t report", "type": "postback", diff --git a/src/webhook/handlers/__tests__/askingArticleSubmissionConsent.test.ts b/src/webhook/handlers/__tests__/askingArticleSubmissionConsent.test.ts index 5fedeef2..dade4fb7 100644 --- a/src/webhook/handlers/__tests__/askingArticleSubmissionConsent.test.ts +++ b/src/webhook/handlers/__tests__/askingArticleSubmissionConsent.test.ts @@ -4,8 +4,9 @@ import type { MockedGa } from 'src/lib/__mocks__/ga'; import type { MockedGql } from 'src/lib/__mocks__/gql'; import MockDate from 'mockdate'; -import askingArticleSubmissionConsent from '../askingArticleSubmissionConsent'; -import { POSTBACK_NO, POSTBACK_YES } from '../utils'; +import askingArticleSubmissionConsent, { + Input, +} from '../askingArticleSubmissionConsent'; import originalGql from 'src/lib/gql'; import originalGa from 'src/lib/ga'; @@ -46,7 +47,7 @@ it('throws on incorrect input', async () => { it('should thank the user if user does not agree to submit', async () => { const inputSession = new Date('2020-01-01T18:10:18.314Z').getTime(); - const params: ChatbotPostbackHandlerParams = { + const params: ChatbotPostbackHandlerParams = { context: { sessionId: inputSession, msgs: [ @@ -56,7 +57,7 @@ it('should thank the user if user does not agree to submit', async () => { postbackData: { sessionId: inputSession, state: 'ASKING_ARTICLE_SUBMISSION_CONSENT', - input: POSTBACK_NO, + input: [], }, userId: 'userId', }; @@ -88,7 +89,7 @@ it('should thank the user if user does not agree to submit', async () => { it('should submit article if user agrees to submit', async () => { const inputSession = new Date('2020-01-01T18:10:18.314Z').getTime(); - const params: ChatbotPostbackHandlerParams = { + const params: ChatbotPostbackHandlerParams = { context: { sessionId: inputSession, msgs: [ @@ -98,7 +99,7 @@ it('should submit article if user agrees to submit', async () => { postbackData: { sessionId: inputSession, state: 'ASKING_ARTICLE_SUBMISSION_CONSENT', - input: POSTBACK_YES, + input: [0], }, userId: 'userId', }; @@ -140,7 +141,7 @@ it('should submit article if user agrees to submit', async () => { it('should submit image article if user agrees to submit', async () => { const inputSession = new Date('2020-01-01T18:10:18.314Z').getTime(); - const params: ChatbotPostbackHandlerParams = { + const params: ChatbotPostbackHandlerParams = { context: { sessionId: inputSession, msgs: [{ id: '6530038889933', type: 'image' }], @@ -148,13 +149,16 @@ it('should submit image article if user agrees to submit', async () => { postbackData: { sessionId: inputSession, state: 'ASKING_ARTICLE_SUBMISSION_CONSENT', - input: POSTBACK_YES, + input: [0], }, userId: 'userId', }; MockDate.set('2020-01-02'); gql.__push({ data: { CreateMediaArticle: { id: 'new-article-id' } } }); + gql.__push({ + data: { CreateAIReply: { text: '' /* Simulate nothing from AI */ } }, + }); const result = await askingArticleSubmissionConsent(params); MockDate.reset(); expect(gql.__finished()).toBe(true); @@ -177,7 +181,7 @@ it('should submit image article if user agrees to submit', async () => { it('should create a UserArticleLink when creating a Article', async () => { const userId = 'user-id-0'; - const params: ChatbotPostbackHandlerParams = { + const params: ChatbotPostbackHandlerParams = { context: { sessionId: 0, msgs: [ @@ -186,7 +190,7 @@ it('should create a UserArticleLink when creating a Article', async () => { }, postbackData: { sessionId: 0, - input: POSTBACK_YES, + input: [0], state: 'ASKING_ARTICLE_SUBMISSION_CONSENT', }, userId, @@ -204,7 +208,7 @@ it('should create a UserArticleLink when creating a Article', async () => { it('should ask user to turn on notification settings if they did not turn it on after creating an Article', async () => { const userId = 'user-id-0'; - const params: ChatbotPostbackHandlerParams = { + const params: ChatbotPostbackHandlerParams = { context: { sessionId: 0, msgs: [ @@ -214,7 +218,7 @@ it('should ask user to turn on notification settings if they did not turn it on postbackData: { sessionId: 0, state: 'ASKING_ARTICLE_SUBMISSION_CONSENT', - input: POSTBACK_YES, + input: [0], }, userId, }; diff --git a/src/webhook/handlers/askingArticleSubmissionConsent.ts b/src/webhook/handlers/askingArticleSubmissionConsent.ts index 18f1e4cb..f338ccbb 100644 --- a/src/webhook/handlers/askingArticleSubmissionConsent.ts +++ b/src/webhook/handlers/askingArticleSubmissionConsent.ts @@ -1,5 +1,4 @@ import { t } from 'ttag'; -import { Message } from '@line/bot-sdk'; import { z } from 'zod'; import { ChatbotPostbackHandler } from 'src/types/chatbotState'; @@ -9,6 +8,7 @@ import { getArticleURL } from 'src/lib/sharedUtils'; import UserSettings from 'src/database/models/userSettings'; import UserArticleLink from 'src/database/models/userArticleLink'; import { + Article, ArticleTypeEnum, SubmitMediaArticleUnderConsentMutation, SubmitMediaArticleUnderConsentMutationVariables, @@ -17,8 +17,6 @@ import { } from 'typegen/graphql'; import { - POSTBACK_YES, - POSTBACK_NO, ManipulationError, createTextMessage, createCommentBubble, @@ -26,9 +24,14 @@ import { createNotificationSettingsBubble, getLineContentProxyURL, createAIReply, + searchText, + searchMedia, + createCooccurredSearchResultsCarouselContents, + setMostSimilarArticlesAsCooccurrence, } from './utils'; -const inputSchema = z.enum([POSTBACK_NO, POSTBACK_YES]); +// Input should be array of context.msgs idx. Empty if the user does not want to submit. +const inputSchema = z.array(z.number().int().min(0)); /** Postback input type for ASKING_ARTICLE_SUBMISSION_CONSENT state handler */ export type Input = z.infer; @@ -50,38 +53,40 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ throw new ManipulationError(t`Please choose from provided options.`); } - const firstMsg = context.msgs[0]; + const msgsToSubmit = context.msgs.filter((_, idx) => input.includes(idx)); + // istanbul ignore if - if (!firstMsg) { - throw new ManipulationError('No message found in context'); // Should never happen + if (msgsToSubmit.length !== input.length) { + throw new ManipulationError('Index range out of bound'); // Should never happen } const visitor = ga( userId, state, - firstMsg.type === 'text' ? firstMsg.text : firstMsg.id + // use the first message in context as representative + context.msgs[0].type === 'text' ? context.msgs[0].text : context.msgs[0].id ); - let replies: Message[] = []; - - switch (input) { - default: - // Exhaustive check - return input satisfies never; + // Abort if user does not want to submit + // + if (msgsToSubmit.length === 0) { + visitor.event({ ec: 'Article', ea: 'Create', el: 'No' }).send(); - case POSTBACK_NO: - visitor.event({ ec: 'Article', ea: 'Create', el: 'No' }); - replies = [ + return { + context, + replies: [ createTextMessage({ text: t`The message has not been reported and won’t be fact-checked. Thanks anyway!`, }), - ]; - break; + ], + }; + } + + visitor.event({ ec: 'Article', ea: 'Create', el: 'Yes' }).send(); - case POSTBACK_YES: { - visitor.event({ ec: 'Article', ea: 'Create', el: 'Yes' }); - let article; - if (firstMsg.type === 'text') { + const createdArticles = await Promise.all( + msgsToSubmit.map(async (msg) => { + if (msg.type === 'text') { const result = await gql` mutation SubmitTextArticleUnderConsent($text: String!) { CreateArticle(text: $text, reference: { type: LINE }) { @@ -91,127 +96,174 @@ const askingArticleSubmissionConsent: ChatbotPostbackHandler = async ({ `< SubmitTextArticleUnderConsentMutation, SubmitTextArticleUnderConsentMutationVariables - >({ text: firstMsg.text }, { userId }); - article = result.data.CreateArticle; - } else { - const articleType: ArticleTypeEnum = uppercase(firstMsg.type); + >({ text: msg.text }, { userId }); + return result.data.CreateArticle; + } - const proxyUrl = getLineContentProxyURL(firstMsg.id); + const articleType: ArticleTypeEnum = uppercase(msg.type); + const proxyUrl = getLineContentProxyURL(msg.id); - const result = await gql` - mutation SubmitMediaArticleUnderConsent( - $mediaUrl: String! - $articleType: ArticleTypeEnum! + const result = await gql` + mutation SubmitMediaArticleUnderConsent( + $mediaUrl: String! + $articleType: ArticleTypeEnum! + ) { + CreateMediaArticle( + mediaUrl: $mediaUrl + articleType: $articleType + reference: { type: LINE } ) { - CreateMediaArticle( - mediaUrl: $mediaUrl - articleType: $articleType - reference: { type: LINE } - ) { - id - } + id } - `< - SubmitMediaArticleUnderConsentMutation, - SubmitMediaArticleUnderConsentMutationVariables - >({ mediaUrl: proxyUrl, articleType }, { userId }); - article = result.data.CreateMediaArticle; - } + } + `< + SubmitMediaArticleUnderConsentMutation, + SubmitMediaArticleUnderConsentMutationVariables + >({ mediaUrl: proxyUrl, articleType }, { userId }); + return result.data.CreateMediaArticle; + }) + ); - /* istanbul ignore if */ - if (!article?.id) { - throw new Error( - '[askingArticleSubmissionConsent] article is not created successfully' - ); - } + /* istanbul ignore if */ + if ( + createdArticles.length === 0 || + !createdArticles.every( + (article): article is Pick => + !!article && article.id !== null + ) + ) { + throw new Error( + '[askingArticleSubmissionConsent] article is not created successfully' + ); + } - await UserArticleLink.createOrUpdateByUserIdAndArticleId( - userId, - article.id - ); + // No need to wait for article-user link to be created + createdArticles.forEach((article) => + UserArticleLink.createOrUpdateByUserIdAndArticleId(userId, article.id) + ); - // Create new session, make article submission button expire after submission - context.sessionId = Date.now(); + // Produce AI reply for all created messages + // + const aiReplyPromises = createdArticles.map((article) => + createAIReply(article.id, userId) + ); - const articleUrl = getArticleURL(article.id); - const articleCreatedMsg = t`Your submission is now recorded at ${articleUrl}`; - const { allowNewReplyUpdate } = await UserSettings.findOrInsertByUserId( - userId - ); + if (context.msgs.length > 1) { + // Search again, this time all messages should be in the database. + // Most similar articles for each respective searched message will be set as cooccurrence. + // + const searchResults = await Promise.all( + context.msgs.map(async (msg) => + msg.type === 'text' + ? searchText(msg.text) + : searchMedia(getLineContentProxyURL(msg.id), userId) + ) + ); + await setMostSimilarArticlesAsCooccurrence(searchResults, userId); - let maybeAIReplies: Message[] = [ + return { + context, + replies: [ createTextMessage({ - text: t`In the meantime, you can:`, + text: t`Thank you for submitting! Now the messages has been recorded in the Cofacts database.`, + }), + createTextMessage({ + text: t`Please choose the messages you would like to view` + ' 👇', }), - ]; - - if (firstMsg.type === 'text') { - const aiReply = await createAIReply(article.id, userId); - - if (aiReply) { - maybeAIReplies = [ - createTextMessage({ - text: '這篇文章尚待查核中,請先不要相信這篇文章。\n以下是機器人初步分析此篇訊息的結果,希望能帶給你一些想法。', - }), - aiReply, - createTextMessage({ - text: '讀完以上機器人的自動分析後,您可以:', - }), - ]; - } - } - - replies = [ - { - type: 'flex', - altText: t`The message has now been recorded at Cofacts for volunteers to fact-check. Thank you for submitting!`, - contents: { - type: 'bubble', - body: { - type: 'box', - layout: 'vertical', - contents: [ - { - type: 'text', - wrap: true, - text: t`The message has now been recorded at Cofacts for volunteers to fact-check. Thank you for submitting!`, - }, - { - type: 'button', - action: { - type: 'uri', - label: t`View reported message`, - uri: articleUrl, - }, - margin: 'md', - }, - ], - }, - }, - }, - ...maybeAIReplies, { type: 'flex', - altText: articleCreatedMsg, + altText: t`Please choose the messages you would like to view`, contents: { type: 'carousel', - contents: [ - createCommentBubble(article.id), - // Ask user to turn on notification if the user did not turn it on - // - process.env.NOTIFY_METHOD && - !allowNewReplyUpdate && - createNotificationSettingsBubble(), - createArticleShareBubble(articleUrl), - ].filter(Boolean), + contents: createCooccurredSearchResultsCarouselContents( + searchResults, + context.sessionId + ), }, }, - ]; - } + ], + }; } - visitor.send(); - return { context, replies }; + // The user only asks for one article + // + const article = createdArticles[0]; + const articleUrl = getArticleURL(article.id); + const articleCreatedMsg = t`Your submission is now recorded at ${articleUrl}`; + + const [aiReply, { allowNewReplyUpdate }] = await Promise.all([ + aiReplyPromises[0], + UserSettings.findOrInsertByUserId(userId), + ]); + + return { + context: { + ...context, + // Create new session, make article submission button expire after submission + // + sessionId: Date.now(), + }, + replies: [ + { + type: 'flex', + altText: t`The message has now been recorded at Cofacts for volunteers to fact-check. Thank you for submitting!`, + contents: { + type: 'bubble', + body: { + type: 'box', + layout: 'vertical', + contents: [ + { + type: 'text', + wrap: true, + text: t`The message has now been recorded at Cofacts for volunteers to fact-check. Thank you for submitting!`, + }, + { + type: 'button', + action: { + type: 'uri', + label: t`View reported message`, + uri: articleUrl, + }, + margin: 'md', + }, + ], + }, + }, + }, + ...(!aiReply + ? [ + createTextMessage({ + text: t`In the meantime, you can:`, + }), + ] + : [ + createTextMessage({ + text: t`This article is still under verification, please refrain from believing it for now. \nBelow is the preliminary analysis result by the bot, hoping to provide you with some insights.`, + }), + aiReply, + createTextMessage({ + text: t`After reading the automatic analysis by the bot above, you can:`, + }), + ]), + { + type: 'flex', + altText: articleCreatedMsg, + contents: { + type: 'carousel', + contents: [ + createCommentBubble(article.id), + // Ask user to turn on notification if the user did not turn it on + // + process.env.NOTIFY_METHOD && + !allowNewReplyUpdate && + createNotificationSettingsBubble(), + createArticleShareBubble(articleUrl), + ].filter(Boolean), + }, + }, + ], + }; }; export default askingArticleSubmissionConsent; diff --git a/src/webhook/handlers/askingCooccurrence.ts b/src/webhook/handlers/askingCooccurrence.ts new file mode 100644 index 00000000..416c2c7e --- /dev/null +++ b/src/webhook/handlers/askingCooccurrence.ts @@ -0,0 +1,209 @@ +import { z } from 'zod'; +import { msgid, ngettext, t } from 'ttag'; +import { FlexSpan } from '@line/bot-sdk'; + +import ga from 'src/lib/ga'; +import { ChatbotPostbackHandler } from 'src/types/chatbotState'; + +import { + POSTBACK_YES, + POSTBACK_NO, + ManipulationError, + createTextMessage, + searchText, + searchMedia, + getLineContentProxyURL, + createPostbackAction, + createCooccurredSearchResultsCarouselContents, + setMostSimilarArticlesAsCooccurrence, +} from './utils'; + +const inputSchema = z.enum([POSTBACK_NO, POSTBACK_YES]); + +/** Minimum similarity for a CooccurredMessage to be considered as existing in DB */ +const IN_DB_THRESHOLD = 0.8; + +/** Postback input type for ASKING_ARTICLE_SOURCE state handler */ +export type Input = z.infer; + +const askingCooccurence: ChatbotPostbackHandler = async ({ + context, + postbackData: { state, input: postbackInput }, + userId, +}) => { + let input: Input; + try { + input = inputSchema.parse(postbackInput); + } catch (e) { + console.error('[askingCooccurence]', e); + throw new ManipulationError(t`Please choose from provided options.`); + } + + const visitor = ga(userId, state, `Batch: ${context.msgs.length} messages`); + + switch (input) { + case POSTBACK_NO: { + visitor + .event({ + ec: 'UserInput', + ea: 'IsCooccurrence', + el: 'No', + }) + .send(); + return { + context, + replies: [ + createTextMessage({ + text: t`Please send me the messages separately.`, + }), + ], + }; + } + + case POSTBACK_YES: { + visitor + .event({ + ec: 'UserInput', + ea: 'IsCooccurrence', + el: 'Yes', + }) + .send(); + + const searchResults = await Promise.all( + context.msgs.map(async (msg) => + msg.type === 'text' + ? searchText(msg.text) + : searchMedia(getLineContentProxyURL(msg.id), userId) + ) + ); + + const notInDbMsgIndexes = searchResults.reduce((indexes, result, idx) => { + const firstResult = result.edges[0]; + if (!firstResult) return [...indexes, idx]; + + return ('mediaSimilarity' in firstResult + ? firstResult.mediaSimilarity + : firstResult.similarity) >= IN_DB_THRESHOLD + ? indexes + : [...indexes, idx]; + }, [] as number[]); + + if (notInDbMsgIndexes.length > 0) { + // Ask if the user want to submit those are not in DB into the database + const totalCount = context.msgs.length; + const inDbStatus = + notInDbMsgIndexes.length === totalCount + ? t`None of the ${notInDbMsgIndexes.length} messages you sent are in the Cofacts database.` + : ngettext( + msgid`Out of the ${totalCount} messages you sent, ${notInDbMsgIndexes.length} is not in the Cofacts database.`, + `Out of the ${totalCount} messages you sent, ${notInDbMsgIndexes.length} are not in the Cofacts database.`, + notInDbMsgIndexes.length + ); + + const btnText = `🆕 ${t`Report to database`}`; + const spans: FlexSpan[] = [ + { + type: 'span', + text: t`${inDbStatus} If you think they are most likely a rumor,`, + }, + { + type: 'span', + text: t`press “${btnText}” to make this message public on Cofacts database `, + color: '#ffb600', + weight: 'bold', + }, + { + type: 'span', + text: t`and have volunteers fact-check it. This way you can help the people who receive the same message in the future.`, + }, + ]; + return { + context, + replies: [ + { + type: 'flex', + altText: t`Be the first to report the message`, + contents: { + type: 'bubble', + body: { + type: 'box', + layout: 'vertical', + spacing: 'md', + paddingAll: 'lg', + contents: [ + { + type: 'text', + wrap: true, + contents: spans, + }, + ], + }, + }, + quickReply: { + items: [ + { + type: 'action', + action: createPostbackAction( + btnText, + notInDbMsgIndexes, + btnText, + context.sessionId, + 'ASKING_ARTICLE_SUBMISSION_CONSENT' + ), + }, + { + type: 'action', + action: createPostbackAction( + t`Don’t report`, + [], + t`Don’t report`, + context.sessionId, + 'ASKING_ARTICLE_SUBMISSION_CONSENT' + ), + }, + ], + }, + }, + ], + }; + } + + // All messages in DB and thus can be set as cooccurrence. + await setMostSimilarArticlesAsCooccurrence(searchResults, userId); + + // Get first few search results for each message, and make at most 10 options + // + + return { + context, + replies: [ + createTextMessage({ + text: `🔍 ${t`There are some messages that looks similar to the ones you have sent to me.`}`, + }), + createTextMessage({ + text: + t`Internet rumors are often mutated and shared. + Please choose the version that looks the most similar` + '👇', + }), + { + type: 'flex', + altText: t`Please choose the most similar message from the list.`, + contents: { + type: 'carousel', + contents: createCooccurredSearchResultsCarouselContents( + searchResults, + context.sessionId + ), + }, + }, + ], + }; + } + + default: + // exhaustive check + return input satisfies never; + } +}; + +export default askingCooccurence; diff --git a/src/webhook/handlers/choosingArticle.ts b/src/webhook/handlers/choosingArticle.ts index 94ecf07e..f1aacfdb 100644 --- a/src/webhook/handlers/choosingArticle.ts +++ b/src/webhook/handlers/choosingArticle.ts @@ -63,48 +63,27 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { const { context, userId, - postbackData: { input, state, sessionId }, + postbackData: { input: selectedArticleId, state, sessionId }, } = params; // Input should be article ID, which is a string - if (typeof input !== 'string') { + if (typeof selectedArticleId !== 'string') { throw new ManipulationError(t`Please choose from provided options.`); } - const firstMsg = context.msgs[0]; - // istanbul ignore if - if (!firstMsg) { - throw new Error('firstMsg is undefined'); // Should never happen - } - - // TODO: handle the case when there are multiple messages in context.msgs + // POSTBACK_NO_ARTICLE_FOUND is only available when context.msgs contain 1 message // - if (input === POSTBACK_NO_ARTICLE_FOUND && firstMsg.type === 'text') { - const visitor = ga(userId, state, firstMsg.text); - visitor.event({ - ec: 'UserInput', - ea: 'ArticleSearch', - el: 'ArticleFoundButNoHit', - }); - visitor.send(); - - const inputSummary = ellipsis(firstMsg.text, 12); - return { - context, - replies: [ - createTextMessage({ - text: - t`I am sorry you cannot find the information “${inputSummary}” you are looking for. But I would still like to help.` + - '\n' + - t`May I ask you a quick question?`, - }), - createArticleSourceReply(context.sessionId), - ], - }; - } - - if (input === POSTBACK_NO_ARTICLE_FOUND && firstMsg.type !== 'text') { - const visitor = ga(userId, state, firstMsg.id); + if (selectedArticleId === POSTBACK_NO_ARTICLE_FOUND) { + const firstMsg = context.msgs[0]; + // istanbul ignore if + if (!firstMsg) { + throw new Error('firstMsg is undefined'); // Should never happen + } + const visitor = ga( + userId, + state, + 'text' in firstMsg ? firstMsg.text : firstMsg.id + ); visitor.event({ ec: 'UserInput', ea: 'ArticleSearch', @@ -112,6 +91,21 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { }); visitor.send(); + if (firstMsg.type === 'text') { + const inputSummary = ellipsis(firstMsg.text, 12); + return { + context, + replies: [ + createTextMessage({ + text: + t`I am sorry you cannot find the information “${inputSummary}” you are looking for. But I would still like to help.` + + '\n' + + t`May I ask you a quick question?`, + }), + createArticleSourceReply(context.sessionId), + ], + }; + } return { context, replies: [ @@ -126,8 +120,6 @@ const choosingArticle: ChatbotPostbackHandler = async (params) => { }; } - const selectedArticleId = input; - await UserArticleLink.createOrUpdateByUserIdAndArticleId( userId, selectedArticleId, diff --git a/src/webhook/handlers/handlePostback.ts b/src/webhook/handlers/handlePostback.ts index 89a9cbbe..e7f036db 100644 --- a/src/webhook/handlers/handlePostback.ts +++ b/src/webhook/handlers/handlePostback.ts @@ -4,6 +4,7 @@ import choosingReply from './choosingReply'; import askingArticleSubmissionConsent from './askingArticleSubmissionConsent'; import askingArticleSource from './askingArticleSource'; import defaultState from './defaultState'; +import askingCooccurence from './askingCooccurrence'; import { ManipulationError } from './utils'; import tutorial from './tutorial'; import { @@ -57,6 +58,10 @@ export default async function handlePostback( result = await askingArticleSubmissionConsent(params); break; } + case 'ASKING_COOCCURRENCE': { + result = await askingCooccurence(params); + break; + } default: { result = defaultState(params); break; diff --git a/src/webhook/handlers/processBatch.ts b/src/webhook/handlers/processBatch.ts index 1fde2276..7a5c1784 100644 --- a/src/webhook/handlers/processBatch.ts +++ b/src/webhook/handlers/processBatch.ts @@ -1,26 +1,55 @@ import { Message } from '@line/bot-sdk'; +import { t } from 'ttag'; import { Context, CooccurredMessage } from 'src/types/chatbotState'; -import { sleep } from 'src/lib/sharedUtils'; -import { createTextMessage } from './utils'; +import { + POSTBACK_NO, + POSTBACK_YES, + createPostbackAction, + createTextMessage, +} from './utils'; async function processBatch(messages: CooccurredMessage[]) { const context: Context = { sessionId: Date.now(), - msgs: [], + msgs: messages, }; + const msgCount = messages.length; + const replies: Message[] = [ - createTextMessage({ - text: `目前我還沒辦法一次處理 ${messages.length} 則訊息,請一則一則傳進來唷!`, - }), + { + ...createTextMessage({ + text: t`May I ask if the ${msgCount} messages above were sent by the same person at the same time?`, + }), + quickReply: { + items: [ + { + type: 'action', + action: createPostbackAction( + t`Yes`, + POSTBACK_YES, + t`Yes, same person at same time`, + context.sessionId, + 'ASKING_COOCCURRENCE' + ), + }, + { + type: 'action', + action: createPostbackAction( + t`No`, + POSTBACK_NO, + t`No, from different person or at different time`, + context.sessionId, + 'ASKING_COOCCURRENCE' + ), + }, + ], + }, + }, ]; - // TODO: initiate multi-message processing here - // - await sleep(1000); // Simulate multi-message processing and see if more message in batch. - return { context, replies }; } diff --git a/src/webhook/handlers/singleUserHandler.ts b/src/webhook/handlers/singleUserHandler.ts index 688bc512..417cb27c 100644 --- a/src/webhook/handlers/singleUserHandler.ts +++ b/src/webhook/handlers/singleUserHandler.ts @@ -33,10 +33,15 @@ const userIdBlacklist = (process.env.USERID_BLACKLIST || '').split(','); const REPLY_TIMEOUT = 58000; /** - * The time of messages stays in the batch. - * The messages sent within this timeout are in the same co-occurrence. + * The amount of time to wait for the next message to arrive before processing the batch. */ -const BATCH_TIMEOUT = 500; // ms +const TIMEOUT_BEFORE_PROCESSING = 500; // ms + +/** + * The amount of time to wait for the next message to arrive before asking if the messages are + * sent by the same person at the same time. + */ +const TIMEOUT_BEFORE_ASKING_COOCCURRENCES = 1000; // ms // A symbol that is used to prevent accidental return in singleUserHandler. // It should only be used when timeout are correctly handled. @@ -168,7 +173,7 @@ const singleUserHandler = async ( ): Promise { await redis.push(REDIS_BATCH_KEY, msg); - await sleep(BATCH_TIMEOUT); + await sleep(TIMEOUT_BEFORE_PROCESSING); if (!(await isLastInBatch(msg))) { // New message appears during we sleep, @@ -176,7 +181,8 @@ const singleUserHandler = async ( return cancel(); } - // Try process the batch and calculate results + // Try processing the batch and calculate results + // const messages: CooccurredMessage[] = await redis.range( REDIS_BATCH_KEY, 0, @@ -184,6 +190,11 @@ const singleUserHandler = async ( ); if (messages.length !== 1) { + // Asking cooccurrences are faster than processing single message in batch. + // To prevent new messages from coming in right after we ask cooccurrences, + // we wait first and check if there are new messages. + // + await sleep(TIMEOUT_BEFORE_ASKING_COOCCURRENCES); return send(await processBatch(messages), msg); } diff --git a/src/webhook/handlers/utils.ts b/src/webhook/handlers/utils.ts index 1e1faf22..d594c8f0 100644 --- a/src/webhook/handlers/utils.ts +++ b/src/webhook/handlers/utils.ts @@ -34,6 +34,9 @@ import type { import type { Input as ChoosingReplyInput } from './choosingReply'; import type { Input as AskingArticleSourceInput } from './askingArticleSource'; import type { Input as AskingArticleSubmissionConsentInput } from './askingArticleSubmissionConsent'; +import type { Input as askingCooccurenceInput } from './askingCooccurrence'; + +const MAX_CAROUSEL_BUBBLE_COUNT = 9; const splitter = new GraphemeSplitter(); @@ -47,6 +50,7 @@ type StateInputMap = { CHOOSING_REPLY: ChoosingReplyInput; ASKING_ARTICLE_SOURCE: AskingArticleSourceInput; ASKING_ARTICLE_SUBMISSION_CONSENT: AskingArticleSubmissionConsentInput; + ASKING_COOCCURRENCE: askingCooccurenceInput; Error: unknown; }; @@ -191,7 +195,7 @@ export function createAskArticleSubmissionConsentReply( color: '#ffb600', action: createPostbackAction( btnText, - POSTBACK_YES, + [0], // The first and the only message btnText, sessionId, 'ASKING_ARTICLE_SUBMISSION_CONSENT' @@ -203,7 +207,7 @@ export function createAskArticleSubmissionConsentReply( color: '#333333', action: createPostbackAction( t`Don’t report`, - POSTBACK_NO, + [], t`Don’t report`, sessionId, 'ASKING_ARTICLE_SUBMISSION_CONSENT' @@ -1119,5 +1123,80 @@ export function createSearchResultCarouselContents( }, }; }) - .slice(0, 9); /* flex carousel has at most 10 bubbles */ + .slice(0, MAX_CAROUSEL_BUBBLE_COUNT); /* Avoid too many bubbles */ +} + +function getSimilarity( + edge: SearchMediaResult['edges'][number] | SearchTextResult['edges'][number] +) { + return 'mediaSimilarity' in edge ? edge.mediaSimilarity : edge.similarity; +} + +export function createCooccurredSearchResultsCarouselContents( + searchResults: (SearchMediaResult | SearchTextResult)[], + sessionId: number +): FlexBubble[] { + const idEdgeMap: Record< + string, + SearchMediaResult['edges'][number] | SearchTextResult['edges'][number] + > = {}; + + // We try to get equal number of items out of every search result, + // starting from the first ranked items from each list. + // + for ( + let idx = 0, depletedSearchResultCount = 0; + Object.keys(idEdgeMap).length < MAX_CAROUSEL_BUBBLE_COUNT && + depletedSearchResultCount < searchResults.length; + idx += 1 + ) { + for (const searchResult of searchResults) { + if (idx == searchResult.edges.length) { + depletedSearchResultCount += 1; + continue; + } else if (idx > searchResult.edges.length) { + continue; + } + + // Update idEdgeMap if the edge is not in the map or has higher similarity + const currentEdge = searchResult.edges[idx]; + if ( + !idEdgeMap[currentEdge.node.id] || + getSimilarity(idEdgeMap[currentEdge.node.id]) < + getSimilarity(currentEdge) + ) { + idEdgeMap[currentEdge.node.id] = currentEdge; + } + } + } + + return createSearchResultCarouselContents( + Object.values(idEdgeMap) + // Sort all edges by similarity + .sort((a, b) => getSimilarity(b) - getSimilarity(a)), + sessionId + ); +} + +/** + * Mark the most similar item in the search of each searched messages as a cooccurrence + * + * @param searchResults - search results from searchMedia() or searchText(), with most similar item + * of each searched items in the first edge. + * @param userId - user that observes this cooccurrence + */ +export function setMostSimilarArticlesAsCooccurrence( + searchResults: (SearchMediaResult | SearchTextResult)[], + userId: string +) { + const articleIds = searchResults.map( + (searchResult) => searchResult.edges[0].node.id + ); + return gql` + mutation SetCooccurrences($articleIds: [String!]!) { + CreateOrUpdateCooccurrence(articleIds: $articleIds) { + id + } + } + `({ articleIds }, { userId }); }