SIPメッセージング

2024年11月7日 (木) 12:11時点におけるTakahashi (トーク | 投稿記録)による版 (さらなる応用)

SIPメッセージで電話機等でメッセージを交換する方法。ブラウザ等を使えばチャット用にも使える。要するにSMSみたいなもん。

目次

Asteriskでの実装

Asteriskは音声通話のみならずSIPメッセージの『交換』にも使えます。実はextensionsの記述でテキストメッセージの配送や処理も行えるのです。
Asterisk 20 サンプル設定ファイル でもこのSIPメッセージングのサンプルを実装しています。

SIP MESSAGE

MESSAGE sip:1000@192.168.254.234 SIP/2.0
Via: SIP/2.0/WSS 192.0.2.191;received=192.168.254.30;branch=z9hG4bK1314248
To: <sip:1000@192.168.254.234>
From: "TAKAHASHI,Takao" <sip:phone33@192.168.254.234>;tag=7m8l4i50nj
CSeq: 1 MESSAGE
Call-ID: glia153j7er66kd35o0s
Max-Forwards: 70
Supported: outbound
User-Agent: Browser Phone 0.3.27 (SIPJS - 0.20.0) Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 
Safari/537.36
Content-Type: text/plain
Content-Length: 7
Content-Type: text/plain
Content-Length:     7

hoge

SIPのMESSAGEによるメッセージ例。

チャネル

基本的にエンドポイント同士は通常のPJSIP/phone123のような形式で認識される。ただし接続した際のチャネルが異なり、音声パスではなく ast_message_queue となる。

${CHANNEL(name)} = Message/ast_msg_queue

SIPでメッセージを受け取ると上記のようなチャネル名となるので、これを判断することで音声通話ではなくSIPメッセージあると判断できる。

メッセージ本体

ファンクションMESSAGE()がメッセージ自体のハンドリングを行う。

MESSAGE(from) r/w

送信元

MESSAGE(to) r/w

送信先

MESSAGE(body) r/w

メッセージの実体

MESSAGE(custom_data) w/o

mark_all_outbound または clear_all_outbound を指定

応用1:Asteriskで何か処理してメッセージで応答する

役に立つのか立たないのかわかりませんが、SIPメッセージで受信した内容に応じて処理する例。ブラウザフォン等のメッセージでの問い合わせに応答させる例。

内線として実装する

まず、この応答処理を内線として定義します。音声で呼ばれた場合は応答して切りますが、この番号宛てのSIPメッセージだった場合にはメッセージ処理へ飛びます。

exten => 1000,1,NoOp
exten => 1000,n,Set(MTARGET=${EXTEN})
exten => 1000,n,GotoIf($["${CHANNEL(name)}"="Message/ast_msg_queue"]?msgdispatch,s,1)
exten => 1000,n,Answer
exten => 1000,n,Morsecode(S)
exten => 1000,n,Hangup

メッセージ処理

メッセージ本体(body)の最初の項目をサービス名、残りを引数と解釈するディスパッチャを用意します。あわせてメッセージの打ち返し先も組み立てておきます。各サービスからの返値は変数、RETVに入っているものとします。

[msgdispatch]
;サービス毎に分岐させ返値を送り返す
exten => s,1,NoOp
;送り先(戻し先)をfromから取得
exten => s,n,Set(SBACK=${CUT(MESSAGE(from),@,1)})
exten => s,n,Set(SBACK=${CUT(SBACK,:,2)})
;ドメインを送り元から取得
exten => s,n,Set(SDOM=${CUT(MESSAGE(from),@,2)})
exten => s,n,Set(SDOM=${CUT(SDOM,>,1)})
;メッセージ本体の頭の部分をサービス名として取得
exten => s,n,Set(SVC=${CUT(MESSAGE(body), ,1)})
exten => s,n,Set(SVC=${TOLOWER(${SVC})})
;サービスに渡す内容はスペースの後ろ
exten => s,n,Set(CONTENT=${CUT(MESSAGE(body), ,2-)})
;サービスに応じてサブルーチンコール
exten => s,n,Gosub(sub-msg-${SVC},s,1)
;返値をメッセージの本体にする
exten => s,n,Set(MESSAGE(body)=${RETV})
;メッセージを打ち返す
exten => s,n,MessageSend(pjsip:${SBACK},${MTARGET},sip:${SBACK}@${SDOM})
exten => s,n,Hangup

各サービス処理

各サービスは sub-msg-サービス名 というかたちでコールされるので必要な処理を用意します。
以下の例ではAsteriskのMATH()、コマンドのuptimeとexprを定義しています。

[sub-msg-math]
exten => s,1,NoOp(MATH)
exten => s,n,Set(RETV=${MATH(${CONTENT},int)})
exten => s,n,Return

[sub-msg-uptime]
exten => s,1,NoOp(UPTIME)
exten => s,n,Set(RETV=${SHELL(uptime)})
exten => s,n,Return

[sub-msg-expr]
exten => s,1,NoOp(EXPR)
exten => s,n,Set(RETV=${SHELL(expr ${CONTENT})})
exten => s,n,Return

使い方

ブラウザフォン等のSIPメッセージで uptime を送るとuptmeの結果が送られてきます。math 1+1 を送るとAsteriskのMATH()で計算されて2が送られてきます。
BP SIP MSG241030.png
こんな感じで、Asteriskを使ってメッセージ処理をすることができます。

応用2:グループチャットを実装する

少し難しい例になりますが、グループチャットをつくってみましょう。これもAsteriskがメッセージ交換をすることで実装できます。

内線をつくる

こちらも内線をつくっておきます。この番号めがけてメッセージを飛ばすことを前提とします。

exten => 1100,1,NoOp
exten => 1100,n,Set(MTARGET=${EXTEN})
exten => 1100,n,GotoIf($["${CHANNEL(name)}"="Message/ast_msg_queue"]?grp-chat,s,1)
exten => 1100,n,Answer
exten => 1100,n,Morsecode(S)
exten => 1100,n,Hangup

この番号に音声ダイヤルしてもプププといって切られるだけですが、メッセージを投げられた場合にはgrp-chatの処理を行います。

グループチャット本体

[grp-chat]
exten => s,1,NoOp(Group Chat)
;グループのメンバ(エンドポイント)をカンマ区切りで
;この部分はAstDBなり外部DB化するなどお好みで
exten => s,n,Set(MYGRP=phone3,phone33,phone34)
;MTARGETに元のextenが入っている(チャットの打ち先)
exten => s,n,NoOp(${MTARGET})
;「誰」が発言したかわからなくなるので元のFromを保存
exten => s,n,Set(OFROM=${MESSAGE(from)})
;元の送信者をメッセージ本体に結合
exten => s,n,Set(MESSAGE(body)=${OFROM} : ${MESSAGE(body)})
;送信元エンドポイント名だけ取り出し(コロンの後ろかつ@の前)
exten => s,n,Set(OENDP=${CUT(OFROM,:,2)})
exten => s,n,Set(OENDP=${CUT(OENDP,@,1)})
;Toからアドレス以降だけ取り出し
exten => s,n,Set(TTO=${MESSAGE(to)})
exten => s,n,Set(DSTA=${CUT(TTO,@,2)})
;取り出しの初期位置は1
exten => s,n,Set(CNT=1)
;グループからカンマ区切りを1つずつ取り出す
exten => s,n(loop),Set(ENDP=${CUT(MYGRP,\,,${CNT})})
;要素がなくなったら終了
exten => s,n,GotoIf($["${ENDP}"=""]?endlp)
;自分から自分へは送らない
exten => s,n,GotoIf($["${ENDP}"="${OENDP}"]?cont)
;送信先CONTACTに変換
exten => s,n,Set(DESTA=${PJSIP_DIAL_CONTACTS(${ENDP})})
;CONTACTSの頭部分を修正(最初のPJSIP/を取る)
exten => s,n,Set(DESTA=${CUT(DESTA,/,2-)})
;送信先組み立て
exten => s,n,Set(DESTB=sip:${ENDP}@${DSTA})
exten => s,n,MessageSend(pjsip:${DESTA},${MTARGET},${DESTB})
;取り出し位置を+1する
exten => s,n(cont),Set(CNT=${INC(CNT)})
exten => s,n,Goto(loop)
exten => s,n(endlp),Hangup

少し難しくなっていますが、カンマ区切りれ列挙されたエンドポイントをグループメンバーとし、それらに同じ内容をSIP MESSAGEで送信します。この際、発信元をAsteriskの『内線』番号としますが、誰が発言したかわからなくなるのでメッセージ本体(body)に元のコンタクトを付加します。一斉送信ではなく順次送信ですが、よほどの数を登録しない限り、まあこれで大丈夫でしょう。
所属している『グループ』なので自分も入っているわけですが、自分から自分へメッセージを送る意味はないので自分には送らないようにします。
SIPM-GRPC.png
こんな感じでちょっとした社内/部署内チャットに応用することができます。

さらなる応用

先ほどこのグループチャットの『番号』として1100を内線番号にしましたが、これは『番号』である必要はありません。以下のように"chat-a"として定義することもできます。

exten => chat-a,1,NoOp
exten => chat-a,n,Set(MTARGET=${EXTEN})
exten => chat-a,n,GotoIf($["${CHANNEL(name)}"="Message/ast_msg_queue"]?grp-chat,s,1)
exten => chat-a,n,Answer
exten => chat-a,n,Morsecode(S)
exten => chat-a,n,Hangup

これはSIPのあたりまえではありますが、SIPのコンタクトは何も内線番号である必要はなく、文字列でもかまわないからです。この場合、ブラウザフォンや電話機等からの「投げ先」要するにコンタクトとしては内線番号ではなく文字列を指定します。
String-contact.png
Asteriskはこのように文字列であっても内線番号であるかのように処理する能力を持っています。数字ではない文字列で処理したい場合に使えます。