Microsoft Excel上で動作するチャットアプリケーションを作っていた話
はじめに
こんにちは。高校2年の樅山です。
2020/11 から始まった、ものづくりをする高校生のための新しいグループ、「Palettte」が主催する Palettte Advent Calendar 2020 の16日目の記事となります。
今回は、高校1年生の時に「情報の科学」という単位の授業中に合間を縫って開発していた、Exchat というアプリケーションについて紹介します。
開発動機
学校のパソコンでは他のパソコンと通信することもできないので、授業内容に関する知見や疑問などについて共有することはできません。
基本的に、授業ではExcelの使い方などを学んでいたので、Excel上で他のパソコンとメッセージをやり取りすることができれば、とても便利だなと感じました。
Exchatは、Microsoft Excelで動作する Visual Basic を用いて、データベースはインストールされていなかった(し、Microsoft Accessのことを知らなかった)ので、テキスト形式でメッセージを保存して、LANに保存する形式を取りました。
メッセージの作成
VBAでは、フォームを簡単に利用できるので、入力フォームから受け取ったメッセージが空でなければ、LANに新しく作成します。
Sub message_submit()
Dim textbox_data As String
textbox_data = UserForm4.TextBox1.Value
If textbox_data = "" Then
textbox_data = UserForm1.TextBox1.Value
End If
If Not textbox_data = "" Then
Dim target_path As String: target_path = ROOT_APP_PATH & "models\message_model\public\"
target_path = target_path & get_current_number_of_files(target_path) + 1 & ".pasta"
Dim message_data As String
message_data = textbox_data
Dim userid As String: userid = init.user_id
Open target_path For Output As #1
Print #1, easySecure.easy_secure(textbox_data)
Print #1, easySecure.easy_secure(Now)
Print #1, easySecure.easy_secure(userid)
Close #1
UserForm1.TextBox1.Value = ""
UserForm4.TextBox1.Value = ""
End If
End Sub
Function get_current_number_of_files(target_path As String)
Dim index As Long: index = 1
While Not Dir(target_path & index & ".pasta") = ""
index = index + 1
Wend
get_current_number_of_files = index - 1
End Function
冷静になると、UserFormにも命名をしないと恐ろしく読みづらいのですが、それに気づかずに毎回困っていました。
ここで、easySecure
についてですが、LANにチャット情報をそのまま保存するのは困るなと思い、
Function easy_secure(ByVal str As String)
Dim password As String: password = "securepass"
Do While Len(str) > Len(password)
password = password & password
Loop
password = Left(password, Len(str))
Dim secured As String
For i = 1 To Len(str)
secured = secured & CStr(Asc(Mid(str, i, 1)) Xor Asc(Mid(password, i, 1))) & " "
Next
easy_secure = secured
End Function
Function easy_r_secure(ByVal str As String)
Dim password As String: password = "securepass"
Do While Len(str) > Len(password)
password = password & password
Loop
password = Left(password, Len(str))
Dim secured_str_arr() As String
Dim raw_string As String
secured_str_arr = Split(str, " ")
For j = LBound(secured_str_arr) To UBound(secured_str_arr) - 1
raw_string = raw_string & Chr(CLng(secured_str_arr(j)) Xor Asc(Mid(password, j + 1, 1)))
Next j
easy_r_secure = raw_string
End Function
とても簡単な文字列XOR暗号化をかけていました。
このままではいけないという危機感があったのか、RSA暗号を実装しかけている痕跡が残っていました。
Function encryption(raw_message As String)
' 文字をshift_jisコードに
Dim shift_jis__array
ReDim shift_jis_array(Len(raw_message))
For i = 1 To Len(raw_message)
shift_jis_array(i) = Asc(Mid$(raw_message, i, 1))
MsgBox Asc(Mid$(raw_message, i, 1))
Next
End Function
Function decryption()
End Function
Sub generate_public_key()
Dim n As Double
Dim public_key_dir As String
public_key_dir = ROOT_APP_PATH & "keys\"
n = generate_prime_number() * generate_prime_number()
Dim WshNetworkObject As Object
Set WshNetworkObject = CreateObject("WScript.Network")
user_id = WshNetworkObject.UserName
public_key_path = public_key_dir + CStr(user_id) + ".key"
Open public_key_path For Output As #1
Print #1, CStr(n)
Close #1
End Sub
' 公開鍵e = 65535 に固定するよ
Sub generate_private_key()
' 秘密鍵dを計算してHomeに保存する
End Sub
' 素数を計算する
' 使った素数をLANに暗号化して登録してチェックしてはじく
Function generate_prime_number()
Randomize
Dim rnd_min As Double: rnd_min = 10000000
Dim rnd_max As Double: rnd_max = 1000000000
Dim prime_number As Double: prime_number = 0
Dim find_prime_number As Boolean: find_prime_number = False
prime_number = Int((rnd_max - rnd_min + 1) * Rnd + rnd_min)
Dim i As Double
Dim j As Double
For i = prime_number To rnd_max
For j = 2 To Int(Sqr(i))
If i Mod j = 0 Then
Exit For
End If
If j = Int(Sqr(i)) Then
find_prime_number = True
End If
Next
If find_prime_number Then
prime_number = i
Exit For
End If
Next
generate_prime_number = prime_number
End Function
1 to 1 チャット
1対1でチャット出来るようにもなっていました。
Public private_final_files As Long
Public private_chat_ref_time
Public pvform_opening As Boolean
Sub private_chat_initialize()
If pvform_opening <> True Then
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
Dim f
Dim target_path As String
target_path = ROOT_APP_PATH & "\models\user_model\"
For Each f In fso.GetFolder(target_path).Files
If Not Replace(Mid(f, 37, 18), ".pasta", "") = "" Then
Dim target_name As String
target_name = Replace(Mid(f, 37, 18), ".pasta", "")
target_name = "@" & target_name
Open (f) For Input As #1
Line Input #1, buf
UserForm8.ComboBox1.AddItem target_name
Close #1
End If
Next f
MsgBox ("Initialize")
private_chat_ref_time = Now
For i = 0 To 10000 '自動更新
Application.OnTime DateAdd("s", i, private_chat_ref_time), "update_form"
Next
Else
pvform_opening = False
End If
End Sub
Sub private_chat_shutdown()
Dim subtime: subtime = DateDiff("s", private_chat_ref_time, Now)
For i = subtime + 5 To 10000
Application.OnTime DateAdd("s", i, private_chat_ref_time), "update_form", , False
Next
End Sub
Sub private_chat_submit()
Dim textbox_data As String
textbox_data = UserForm8.TextBox1.Value
If Not textbox_data = "" Then
Dim message_data As String
message_data = textbox_data
Dim userid As String: userid = init.user_id
Dim target_path As String
target_path = ROOT_APP_PATH & "models\message_model\" & userid & "\" & Replace(UserForm8.ComboBox1.Value, "@", "") & "\"
target_path = target_path & get_current_number_of_files(target_path) + 1 & ".pasta"
Open target_path For Output As #1
Print #1, easySecure.easy_secure(textbox_data)
Print #1, easySecure.easy_secure(Now)
Print #1, easySecure.easy_secure(userid)
Close #1
target_path = ROOT_APP_PATH & "models\message_model\" & Replace(UserForm8.ComboBox1.Value, "@", "") & "\" & userid & "\"
target_path = target_path & get_current_number_of_files(target_path) + 1 & ".pasta"
Open target_path For Output As #1
Print #1, easySecure.easy_secure(textbox_data)
Print #1, easySecure.easy_secure(Now)
Print #1, easySecure.easy_secure(userid)
Close #1
UserForm8.TextBox1.Value = ""
End If
Call private_talk.update_form
End Sub
Sub update_form()
pvform_opening = True
Dim to_user As String: to_user = Replace(UserForm8.ComboBox1.Value, "@", "")
Dim target_path As String: target_path = ROOT_APP_PATH & "models\message_model\"
target_path = target_path & init.user_id & "\" & to_user & "\"
Dim number_of_files As Long
number_of_files = public_timeline.get_current_number_of_files(target_path)
If Not Dir(target_path & private_final_files + 1 & ".pasta") = "" Then
UserForm8.ListBox1.Clear
Dim column_index As Long: column_index = 1
For i = number_of_files To 1 Step -1
column_index = 1
Open (target_path & i & ".pasta") For Input As #1
Do Until EOF(1)
Line Input #1, buf
If column_index = 1 Then
UserForm8.ListBox1.AddItem ""
UserForm8.ListBox1.List(UserForm8.ListBox1.ListCount - 1, 1) = easySecure.easy_r_secure(buf)
ElseIf column_index = 2 Then
UserForm8.ListBox1.List(UserForm8.ListBox1.ListCount - 1, 2) = easySecure.easy_r_secure(buf)
Else
UserForm8.ListBox1.List(UserForm8.ListBox1.ListCount - 1, 0) = easySecure.easy_r_secure(buf)
End If
column_index = column_index + 1
Loop
Close #1
UserForm8.ListBox1.List(UserForm8.ListBox1.ListCount - 1, 0) = get_nickname(CStr(UserForm8.ListBox1.List(UserForm8.ListBox1.ListCount - 1, 0))) & " (@" & UserForm8.ListBox1.List(UserForm8.ListBox1.ListCount - 1, 0) & ")"
Next
private_final_files = number_of_files
End If
End Sub
Sub private_chat_change_user()
Dim target_path As String
target_path = ROOT_APP_PATH & "models\message_model"
If Dir(target_path & "\" & init.user_id, vbDirectory) = "" Then
MkDir target_path & "\" & init.user_id
End If
If Dir(target_path & "\" & init.user_id & "\" & Replace(UserForm8.ComboBox1.Value, "@", ""), vbDirectory) = "" Then
MkDir target_path & "\" & init.user_id & "\" & Replace(UserForm8.ComboBox1.Value, "@", "")
End If
If Dir(target_path & "\" & Replace(UserForm8.ComboBox1.Value, "@", ""), vbDirectory) = "" Then
MkDir target_path & "\" & Replace(UserForm8.ComboBox1.Value, "@", "")
End If
If Dir(target_path & "\" & Replace(UserForm8.ComboBox1.Value, "@", "") & "\" & init.user_id, vbDirectory) = "" Then
MkDir target_path & "\" & Replace(UserForm8.ComboBox1.Value, "@", "") & "\" & init.user_id
End If
private_final_files = 0
Call private_talk.update_form
End Sub
ユーザー検知
今、誰がExchatにログインしているかどうかを確かめられるようになっていました。
Sub online()
Dim target_path As String
target_path = ROOT_APP_PATH & "models\user_model\online\"
Open (target_path & init.user_id & ".pasta") For Output As #1
Print #1, "1"
Close #1
End Sub
Sub offline()
Dim target_path As String
target_path = ROOT_APP_PATH & "models\user_model\online\"
Open (target_path & init.user_id & ".pasta") For Output As #1
Print #1, "0"
Close #1
End Sub
Sub search_online_user()
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
Dim f
Dim target_path As String
target_path = ROOT_APP_PATH & "models\user_model\online"
UserForm3.ListBox1.Clear
For Each f In fso.GetFolder(target_path).Files
If Not Replace(Mid(f, 44, 25), ".pasta", "") = "" Then
Dim target_name As String
target_name = get_nickname(Replace(Mid(f, 44, 25), ".pasta", ""))
Open (f) For Input As #1
Line Input #1, buf
If buf = 1 Then
UserForm3.ListBox1.AddItem ""
UserForm3.ListBox1.List(UserForm3.ListBox1.ListCount - 1, 0) = target_name & " (@" & Replace(Mid(f, 44, 25), ".pasta", "") & ")"
End If
Close #1
End If
Next f
End Sub
バックアップ
LAN内ではディレクトリの移動が激しく、よく壊れたので、バックアップが各生徒のみがアクセスできる自分のディレクトリに自動で取られ、復旧できるようになっていました。
Sub backup()
If Dir("Z:\backup", vbDirectory) = "" Then
MkDir ("Z:\backup")
End If
Dim target_path As String
target_path = "Z:\backup\" & Format(Now, "yyyy-mm-dd-hh-mm-ss")
MkDir (target_path)
Dim PUBLIC_CHAT_PATH As String
PUBLIC_CHAT_PATH = ROOT_APP_PATH & "models\message_model\public\"
Dim number_of_files As Long
number_of_files = public_timeline.get_current_number_of_files(PUBLIC_CHAT_PATH)
Dim chat_data_array() As String
ReDim chat_data_array(number_of_files, 4)
Dim chat_data_index As Long
chat_data_index = 0
For i = 1 To number_of_files
Open (PUBLIC_CHAT_PATH & i & ".pasta") For Input As #1
chat_data_index = 0
Do Until EOF(1)
Line Input #1, buf
chat_data_array(i - 1, chat_data_index) = buf
chat_data_index = chat_data_index + 1
Loop
Close #1
Next
Open (target_path & "\public_chat.pasta") For Output As #1
For i = 0 To number_of_files - 1
Print #1, chat_data_array(i, 0)
Print #1, chat_data_array(i, 1)
Print #1, chat_data_array(i, 2)
Print #1, chat_data_array(i, 3)
Next
Close #1
End Sub
送信取り消し
送信取り消しが実装されていました。
Private Sub ListBox1_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
Dim all_messages_count As Long
all_messages_count = public_timeline.get_current_number_of_files(ROOT_APP_PATH & "models\message_model\public\")
Dim target_id As Long
target_id = all_messages_count - ListBox1.ListIndex
Dim sending_user As String
Dim column_index As Long
column_index = 1
Dim raw_row1 As String
Dim raw_row2 As String
Dim raw_row3 As String
Open (ROOT_APP_PATH & "models\message_model\public\" & target_id & ".pasta") For Input As #1
Do Until EOF(1)
Line Input #1, buf
If column_index = 1 Then
raw_row1 = buf
ElseIf column_index = 2 Then
raw_row2 = buf
ElseIf column_index = 3 Then
sending_user = easySecure.easy_r_secure(buf)
raw_row3 = buf
End If
column_index = column_index + 1
Loop
Close #1
If init.user_id = sending_user Then
If MsgBox("送信取り消ししますか?(develop v.6.4より前のバージョンを利用するユーザーには取り消しされません)", vbOKCancel) = vbOK Then
Open (ROOT_APP_PATH & "models\message_model\public\" & target_id & ".pasta") For Output As #1
Print #1, raw_row1
Print #1, raw_row2
Print #1, raw_row3
Print #1, "delete"
Close #1
End If
UserForm4.ListBox1.Clear
For i = number_of_files To 1 Step -1
column_index = 1
Open (target_path & i & ".pasta") For Input As #1
Do Until EOF(1)
Line Input #1, buf
If column_index = 1 Then
UserForm4.ListBox1.AddItem ""
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 1) = easySecure.easy_r_secure(buf)
ElseIf column_index = 2 Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 2) = easySecure.easy_r_secure(buf)
ElseIf column_index = 3 Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) = easySecure.easy_r_secure(buf)
ElseIf column_index = 4 Then
If buf = "delete" Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 1) = "<送信者によってメッセージが取り消されました>"
End If
End If
column_index = column_index + 1
Loop
Close #1
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) = get_nickname(CStr(UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0))) & " (@" & UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) & ")"
Next
final_files = number_of_files
ElseIf init.student_number = "160004" Then
If MsgBox("管理者権限で削除しますか?(develop v.6.4より前のバージョンを利用するユーザーには取り消しされません)", vbOKCancel) = vbOK Then
Open (ROOT_APP_PATH & "models\message_model\public\" & target_id & ".pasta") For Output As #1
Print #1, raw_row1
Print #1, raw_row2
Print #1, raw_row3
Print #1, "admin-delete"
Close #1
End If
UserForm4.ListBox1.Clear
For i = number_of_files To 1 Step -1
column_index = 1
Open (target_path & i & ".pasta") For Input As #1
Do Until EOF(1)
Line Input #1, buf
If column_index = 1 Then
UserForm4.ListBox1.AddItem ""
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 1) = easySecure.easy_r_secure(buf)
ElseIf column_index = 2 Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 2) = easySecure.easy_r_secure(buf)
ElseIf column_index = 3 Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) = easySecure.easy_r_secure(buf)
ElseIf column_index = 4 Then
If buf = "admin-delete" Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 1) = "<管理者によってメッセージが削除されました>"
End If
End If
column_index = column_index + 1
Loop
Close #1
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) = get_nickname(CStr(UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0))) & " (@" & UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) & ")"
Next
final_files = number_of_files
End If
End Sub
メッセージの自動更新
他のユーザーによって送信されたメッセージは、自動更新されて表示されていたようです。
Sub Update()
On Error GoTo Er_line
Dim target_path As String: target_path = ROOT_APP_PATH & "models\message_model\public\"
Dim number_of_files As Long
number_of_files = public_timeline.get_current_number_of_files(target_path)
If Not Dir(target_path & final_files + 1 & ".pasta") = "" Then
UserForm4.ListBox1.Clear
Dim column_index As Long: column_index = 1
For i = number_of_files To 1 Step -1
column_index = 1
Open (target_path & i & ".pasta") For Input As #1
Do Until EOF(1)
Line Input #1, buf
If column_index = 1 Then
UserForm4.ListBox1.AddItem ""
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 1) = easySecure.easy_r_secure(buf)
ElseIf column_index = 2 Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 2) = easySecure.easy_r_secure(buf)
ElseIf column_index = 3 Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) = easySecure.easy_r_secure(buf)
ElseIf column_index = 4 Then
If buf = "delete" Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 1) = "<送信者によってメッセージが取り消されました>"
ElseIf buf = "admin-delete" Then
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 1) = "<管理者によってメッセージが削除されました>"
End If
End If
column_index = column_index + 1
Loop
Close #1
UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) = get_nickname(CStr(UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0))) & " (@" & UserForm4.ListBox1.List(UserForm4.ListBox1.ListCount - 1, 0) & ")"
Next
final_files = number_of_files
End If
target_path = ROOT_APP_PATH & "models\information_model\"
If Not Dir(target_path & info_number_of_files + 1 & ".pasta") = "" Then
Dim info_data As String
Dim line_index As Long: line_index = 1
Dim error_data As Boolean
Open (target_path & info_number_of_files + 1 & ".pasta") For Input As #1
Do Until EOF(1)
Line Input #1, buf
If line_index = 1 Then
info_data = easySecure.easy_r_secure(buf)
Else
error_data = CBool(buf)
End If
line_index = line_index + 1
Loop
Close #1
If error_data Then
MsgBox info_data, vbCritical
Else
MsgBox info_data, vbOKOnly
End If
info_number_of_files = info_number_of_files + 1
End If
Exit Sub
Er_line:
Console ("エラー内容: " & Err.Description)
End Sub
終わりに
一年ぶりにコードを読んでみると、酷いものだなと思いながら、よくExcelでこんなアプリケーションを保守していたなと思います。
突然動かなくなったり(主にフォルダが動かされるのが原因だった)、機能を追加したら他の機能が死んだり、典型的な "ヤバい" 開発の沼にはまって泣きながら学校に残って作業していたことを思い出します。
当時から
Sub Public_New(ByRef content As String)
If Present_(content) Then
Const LATEST_NUM As Long = Get_Files_Count_(PUBCHAT_CONTENT_PATH) + 1
Open PUBCHAT_CONTENT_PATH & LASTEST_NUM & PASTA For Output As #1
Print #1, content
Close #1
Open PUBCHAT_TIME_PATH & LASTEST_NUM & PASTA For Output As #1
Print #1, Now
Close #1
Open PUBCHAT_USERID_PATH & LASTEST_NUM & PASTA For Output As #1
Print #1, init.user_id
Close #1
Open PUBCHAT_STATUS_PATH & LASTEST_NUM & PASTA For Output As #1
Print #1, "normal"
Close #1
content = ""
End If
End Sub
Sub test_submit()
Public_New (Cells(1, 1).Value)
End Sub
Private Function Present_(ByVal data As String)
If data = "" Then
Present_ = False
Else
Present_ = True
End If
End Function
リファクタリングに取り組もうとはしていたみたいです。
実際、Visual Basic for Applicationの開発環境はお世辞にも良いと言えるものではなく、かなりストレスが溜まりました。
しかし、このアプリケーションによって授業中に知見交換ができたり、タイピング練習ができたり(機能に入っていました)、なかなか楽しかったので作ってよかったなと思います。
みなさんも、プログラミング始めたての頃に作ったアプリケーションを掘り出してみると、楽しいかもしれません。