feature image

2021年3月24日 | ブログ記事

traQのメンション・チャンネルリンク機能について【新歓ブログリレー16日目】

こんにちは。20BのSysAd班の@reyuです。

この記事は新歓ブログリレー2021の16日目の記事です。そして、SysAdTechBlogの第3回目の記事です。

3といえばtraQ v3ことtraQ-Sですね。というわけで今回は部内向けのチャットサービスであるtraQについての話です。

traQについて

traQとは、traPで独自に開発・運用している部内向けのチャットサービスです。

詳細については以下の記事などに書かれています。

部内製チャットサービス「traQ」UIのこれまで 【AdC2019 35日目】
この記事は東京工業大学デジタル創作同好会traP アドベントカレンダー2019の35日目の記事で、traQのUI記事シリーズの前編です。後編はこちら: 部内製チャットサービス「traQ」UIのこれから 【AdC2019 53日目】 [/post/918/] -------------------------------------------------------------------------------- こんにちは、@spa (Twitter: @__spaspa__ [https://twitter.com/__spaspa__])です。traPではSysAd班・グラフ…
traQの歴史や、特徴的な機能などについて書かれた記事
部内製チャットサービス「traQ」UIのこれから 【AdC2019 53日目】
この記事は東京工業大学デジタル創作同好会traP アドベントカレンダー2019 [/tag/advent-calendar-2019/]の53日目の記事で、traQのUI記事シリーズの後編です。 traQって何?という人は、前編にあたる『部内製チャットサービス「traQ」UIのこれまで』 [/post/886/]から読むといいかもしれません。 -------------------------------------------------------------------------------- こんにちは、sigma (twitter:@unipota [https://twitter.com/unipota…
traQ-S 開発の経緯などに関する記事
爆☆誕 traQ-S【新歓ブログリレー2020 54日目】
この記事はtraP新歓ブログリレー54日目の記事です。 ‌ ‌ こんにちは、@spa (Twitter: @__spaspa__ [https://twitter.com/__spaspa__])です。最近4月を迎えたと思ったらもう5月です。 さて、このたび部内SNSサービス「traQ」の新バージョンである、「traQ-S」をリリースしました!🎉 この新しいtraQでは、UIの全面的な刷新や各種新機能などが目白押しです!早速見ていきましょう!!!!!! traPtitech/traQ_S-UItraQ S - traP Internal Messenger…
traQ-Sリリース時(約1年前)の記事

さて、traQは数多くの機能を搭載していますが、そのうちの1つにメンション機能があります。例えば以下のようなメッセージを送信すると...

@traP と書くとメンションになる

自分 @reyu は強調される

存在しないユーザー @hoge は置換されない

:@reyu: はユーザーアイコンのスタンプになる

```
コードブロック内で @reyu のようにメンションしても置換されない
```
このように表示されます

また、チャンネルリンクについても同様に、いい感じに置換されます。

仕組み

メッセージ送信時の処理

メッセージ送信時には、メッセージ内からメンションやチャンネルリンクを探し、! + JSON文字列 の形に置換しています。

例 :
@reyu!{"type":"user","raw":"@reyu","id":"5797d23b-ba40-4fb7-8107-69d0b2ac2da4"}
#gps/times/reyu!{"type":"channel","raw":"#gps/times/reyu","id":"34e27bfd-1d2f-485d-8880-7a2f33dcd7ae"}

この処理はクライアントだけでなくサーバーにも実装されていて、サーバーではリクエスト内に embed=true が存在する場合にこの置換を行います。
これによって、ユーザーが制作したBotなどから投稿されたメッセージについても同様の置換を行うことができます。

traPtitech/traQ
traQ - traP Internal Messenger Application Backend - traPtitech/traQ
サーバーでの処理
traPtitech/traQ_S-UI
traQ S - traP Internal Messenger Application Frontend - traPtitech/traQ_S-UI
クライアントでの処理

メッセージ受信時の処理

受信時には、逆にJSON文字列をパースして表示しています。

traQはMarkdown記法に対応していて、そのパースにmarkdown-itやそのプラグインを利用していることもあり、JSON文字列のパースもmarkdown-itのプラグインとして実装されています。

これには、markdown-it-jsonが利用されています。これは、パース対象を絞り込む関数 validate() とパース方法を指定する関数 transform() を渡すことで、!{ ... } 形式のJSON文字列をパースするmarkdown-itのプラグインを実装できるライブラリです。

validate() はJSONをオブジェクトとして受け取り、そのJSONをパースするかどうかをbool型で返します。traQでは以下のようになっています。

export const validate = (data: Readonly<unknown>): data is ValidStructData => {
  if (!isStructData(data)) {
    return false
  }

  const { type, id } = data
  return (
    type === 'user' ||
    (type === 'channel' && !!store.getChannel(id)) ||
    type === 'group'
  )
}

transform()state と JSONオブジェクトを受け取り、必要に応じて state に対して操作を行います。

traQでは以下のようになっています(長いのでユーザーメンション以外については省略しています)。

const transformUser: TransformFunc = (state, { type, id, raw }) => {
  const attributes: [string, string][] = []
  const me = store.getMe()

  attributes.push(['href', `javascript:openUserModal('${id}')`])

  if (id === me?.id) {
    attributes.push(['class', 'message-user-link-highlight message-user-link'])
  } else {
    attributes.push(['class', 'message-user-link'])
  }

  let t = state.push('traq_extends_link_open', 'a', 1)
  t.attrs = attributes
  t.meta = { type, data: id }
  t = state.push('text', '', 0)
  t.content = raw
  state.push('traq_extends_link_close', 'a', -1)
}


const transform: TransformFunc = (state, data) => {
  if (data.type === 'user') {
    transformUser(state, data)
    return
  }
}

state.push()state.tokensToken型のオブジェクトを追加し、追加したオブジェクトを返します(参考1 参考2)。オブジェクトは再代入されるまでは同じ参照先を持つことから、以下のコードのように変数 t に返り値を入れ、それに対して操作をすることで state.tokens の要素を変更することができます。

traPtitech/traq-markdown-it
Markdown parser for traQ. Contribute to traPtitech/traq-markdown-it development by creating an account on GitHub.
markdown-it のプラグインをexportしている
traPtitech/traq-markdown-it
Markdown parser for traQ. Contribute to traPtitech/traq-markdown-it development by creating an account on GitHub.
上でのexportを読み込んでいる

バグ対応 : アイコンスタンプとの衝突

内容がないよう...って感じになってしまったので、実際に発生したバグとそれへの対応を紹介します。

traQでは、:@username: と書くことでユーザーアイコンのスタンプを送信することができます。当然これがメンションになると困るのですが...

となってしまったことがありました(1年近く前の話ですが)。

なんで?

@username あああ だけでなく @usernameあああ なども置換するようにするための修正が原因でバグが起きていました。

この修正では、以下の順に処理を行うようにしていました。

  1. 正規表現 [@@](\S{1,32}) を用いてマッチする文字列を取得する

  2. そのユーザー(もしくはグループ)が存在していたら、置換処理を行う

  3. 存在していなかったら、上で取得した文字列に対して正規表現 ^[@@]([a-zA-Z0-9_-]{1,32}) を用いてマッチする文字列を取得する

  4. そのユーザーが存在していたら、置換処理を行う

一見すると二度手間にも見えますが、グループ名に使える文字の種類がユーザー名より多いことからこのような処理となっています。

これによって上記のバグが発生していました。

  1. :@username:ほげほげ ふがふが → @username:ほげほげ

  2. username:ほげほげ というユーザー(もしくはグループ)は存在しないので、何もしない

  3. @username:ほげほげ@username

  4. username というユーザーは存在するので、置換される

対応

1つ目の正規表現を :?[@@](\S{1,32}) とすることによって、@ の前に : が存在する場合はそこも含めてマッチするようにしました。これによって、@ の前に : が存在するような場合を分類して除くことができます。

修正

バグ対応 : spoiler との衝突

traQでは、markdown-it-spoilerという独自のプラグインを使用しています。例えば !!hoge!! というメッセージを送信すると...

このように見える
クリックすると中身が見える

上でも書いたように、traQのメンションやチャンネルリンクは ! + JSON文字列 として表されています。いかにも衝突しそうな雰囲気がしますね。

した

やっぱりしました。

  1. !!@username!! というメッセージを投稿する

  2. 置換されて、!!!{ ... }!! のような文字列となる

  3. !!! の部分が ! + !! のように扱われてしまい、! + spoiler のようになってしまう

対応

markdown-it-spoilerでは、例えば !!! のように奇数個の ! が連続してあったとき、最初の1つを先に処理してから2つずつ見るようにしていました。

  if (len % 2) {
    const token = state.push("text", "", 0)
    token.content = ch // ch == '!'
    len--
  }

  for (let i = 0; i < len; i += 2) {
  	// ...

修正によって、frontPriorMode=true のときには前から2つずつ見ていき、奇数個の時は最後の ! をパースに含めないようになりました。

  let isOdd = false
  if (len % 2) {
    isOdd = true
    if (!frontPriorMode) {
      const token = state.push("text", "", 0)
      token.content = ch
    }
    len--
  }

  for (let i = 0; i < len; i += 2) {
  	// ...
  state.pos += scanned.length
  if (isOdd && frontPriorMode) {
    state.pos--
  }

おわり

明日の担当者は @Ras です。お楽しみに!

reyu icon
この記事を書いた人
reyu

いろいろやったりやらなかったりしてます。

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2021年3月19日
traPグラフィック班の活動紹介
NABE icon NABE
2023年7月15日
2023 春ハッカソン 06班 stamProlog
H1rono_K icon H1rono_K
2021年4月2日
DXライブラリで重力パズルゲームを作る
Macky1_2 icon Macky1_2
2023年9月13日
ブログリレーを支えるリマインダー
H1rono_K icon H1rono_K
2022年4月5日
アーキテクチャとディレクトリ構造
mazrean icon mazrean
2021年5月16日
CPCTFを支えたインフラ
mazrean icon mazrean
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記