注意
初心者のくせして脱線に脱線を重ねた結果、出来上がったものです
クオリティの低さが目立ちますが、投稿して、改善したいと思っています。
初心者脱却をしたい・・・!
1月から本格的にSkypeで勉強会に参加してから早くも8ヶ月が経過しようとしていますが・・・全く何も進歩が感じられない・・・!そこで、初学者用チュートリアルがあればクリアしたい・・・と思っていたら、トレンドにこんな記事が・・・!
最初は、TwitterAPIを使って情報取得やツイートをしてみようという内容もあったのですが、私はマストドンしか使ってなかったので、マストドンのAPIを叩きたい!!と思いました(初心者脱却よりも脱線をどうにかしろよという話ですが)
そこで、C#でマストドンを動かせるものを探してきて、コンソールでトゥートできるプログラムを作成しました(これもGUIでの実装のためにどう動かすのかの試用としての意味もあります)
TodoリストをGUIで作成することが課題となったわけですが・・・これを作る前に、コンソールでどう動くかを考えてみてヒントにしてみよう・・・と考えたわけです。そうしたら、作ったマストドンのコンソールアプリと組み合わせて、TodoListを配信するアプリを作成してみようと思ったわけです。
何を設計するのか
追加、削除
まずはTodoを登録、追加する機能の実装が必要です。それらの実装はリストを使うことで、登録数の制限をなくすことができます。また、配列番号をIDのように取り扱う事も可能ですね。
リストを閲覧する
リスト閲覧モードも当然必要でしょう。すべてを表示するようにforeachなどでくくれば簡単です。
挿入(開発中)
挿入モードがあると、優先順位でリストを作れるので便利でしょう。うまくいかないので、まだこれが実装中なのを忘れてました(汗)
セーブ、ロードモード
これらのTodoをすべて書き直すことなど意味がありません。セーブ、ロードモードも実装しましょう。
リストをトゥート(手動)
こちらはMastdotというAPIを使用すると、トゥートすることが可能です。先ほどの閲覧モードを呼び出して、すべて一つの変数に代入してから、その内容をトゥートするような設計にしました。
リストをトゥート(時間で自動配信)
このモードがあれば、SNSをボケーとみていた時でも、リマインダのような働きをして、「やっべSNSしてる場合じゃねぇや」って思い出す事も可能ではないかなと効果が期待できます。(こじつけ)
しかし、これが一番苦労しました。なんせ、Taskの概念わかってないから、キャンセルの機能実装ができなかった・・・
やっとの思いでできるようになりました。
アラートモード(自動配信)
先ほどの自動配信モードに対して、こちらはTodoの中の一つだけを通知してくれる機能があればまた便利ではないか。そう考えたので、作ってみることにしました。
#モード選択
すっごく長いので、冗長すぎる表現もあるかと思います。その際にはご指摘いただけると幸いです。
基本的にはモードセレクトから選んでもらう形になっています。
public void ModeSelect(int id, Program p, TodoEditMode edit){
Console.Write(
@"Please select Mode
(Add mode:a, Insert_Mode:i, TootMode(Manual):t, View_Mode:v,
exit:e, save:s, road:r, delete:d AutoTootMode:at):");
var Mode = Console.ReadLine();
switch (Mode)
{
case "a":
id++;
edit.Adding(id, p);
break;
case "i":
edit.Insert(p);
break;
case "v":
edit.View(p);
ModeSelect(id, p, edit);
break;
case "e":
return;
case "t":
var token = OAuth(p);
Toot("qiitadon.com", token, p);
ModeSelect(id, p, edit);
break;
case "s":
Save(p);
ModeSelect(id, p, edit);
break;
case "r":
Road(p);
ModeSelect(id, p, edit);
break;
case "d":
edit.View(p);
Console.WriteLine("Please Write id to delete");
int i = int.Parse(Console.ReadLine());
edit.Delete(i, p, edit);
ModeSelect(id, p, edit);
break;
case "at":
var t = DateTime.Now;
Console.Write("Please Write Interval Minit to Toot Todo:");
int interval = int.Parse(Console.ReadLine());
Console.WriteLine("Press to Quit a key");
int boot_time = t.Minute;
AutoToot(interval, p, boot_time);
ModeSelect(id,p,edit);
break;
case "al":
t = DateTime.Now;
Console.Write("Please Write Interval Minit to Toot Todo:");
interval = int.Parse(Console.ReadLine());
edit.View(p);
Console.WriteLine("Please write ID to Toot Alerm");
id = int.Parse(Console.ReadLine());
Console.WriteLine("Press to Quit a key");
boot_time = t.Minute;
Alerm(interval, p, boot_time,id);
ModeSelect(id, p, edit);
break;
}
}
}
}
Switch関数で囲み、必要なデータはこの関数からすべて取得できるようにしてから、値を渡しています。
ちなみに登録、削除、一覧表示モードについては以下のように書きました。
using System;
using System.Collections.Generic;
namespace TodoListDon
{
class TodoEditMode
{
Program p = new Program();
int id;
public TodoEditMode(int id, Program p, List<string> TodoList)
{
p = this.p;
id = this.id;
TodoList = p.TodoList;
}
public void Adding(int id, Program p)
{
Console.WriteLine("Please Write Todo");
var AddTodo = Console.ReadLine();
p.TodoList.Add(AddTodo);
if (p.Conform() == true)
{
id++;
Adding(id, p);
}
else
{
p.ModeSelect(id, p, this);
}
}
public void Insert(Program p)
{
Console.WriteLine("Please Write ID to move top");
View(p);
id = int.Parse(Console.ReadLine());
string value = p.TodoList[id];
p.TodoList.Insert(1, value);
Delete(id, p, this);
if (p.Conform() == true)
{
Insert(p);
}
else
{
p.ModeSelect(0, p, this);
}
}
public void View(Program p)
{
int i = 0;
foreach (var s in p.TodoList)
{
i++;
Console.WriteLine(i + ":" + s);
}
}
//モードセレクトのクラスを作るしかない。それで、最後に確認をやって確認がTrueなら
public void Delete(int id, Program p, TodoEditMode edit)
{
p.TodoList.RemoveAt(id - 1);
if (p.Conform() == true)
{
Console.WriteLine("Please Write id to delete");
int i = int.Parse(Console.ReadLine());
this.Delete(id, p, this);
}
else
{
p.ModeSelect(0, p, this);
}
}
}
}
これについては、リストの追加、foreach(これに関してはラムダ式でも可能なはず)、削除の機能を利用したので、特筆すべき点は見当たらないと思われます。
#トゥートモードについて
まずはトゥートモードについてです。トゥートモードは、MastdotというAPIを使用します。Mastdotはyamachuさんが作った、MastodonのAPiです。「C# Library for Mastodon API. Easy Toot!」というように、簡単にトゥートが出来る環境を作れてしまいます。
まずは認証
認証はGitHubの説明書き通り、「ApplicationManager クラスの RegistApp メソッドを通して行います.」
var registeredApp = await ApplicationManager.RegistApp("Host name", "Your Application Name", Scope.Read | Scope.Write | Scope.Follow);
・・・ここで、Macユーザーの方が開発を行っているのであれば注意していただきたいのですが、なぜか(それを分かれよという話かもですが)、この記述では、クラスの名前が違ってしまっているらしく、以下の記述で通ります
var registeredApp = await ApplicaionManager.RegistApp("Host name", "Your Application Name", Scope.Read | Scope.Write | Scope.Follow);
ApplicationManager => ApplicaionManagerにしないと、ダメなようでした。
アクセストークンの取得
アクセストークンは、メールアドレスでの認証と、認証ページの表示(OAuth)のいずれかで可能です。今回は認証ページを使用しております。これを実行すると、アクセストークン生成のためのURLを生成します。
var url = ApplicationManager.GetOAuthUrl(registeredApp);
このURLをコンソール上で表示し、コピーペーストで、認証ページを表示させます。認証を承諾し、コピーする旨を書かれるので、コピーしましょう。
そして、そのコードをコンソールリードラインで読み込み、アクセストークンを生成します。
これで、やっと認証完了です。以下にコードを書いておきます。
static string OAuth(Program p){
try
{
//"C:\test\1.txt"をShift-JISコードとして開く
System.IO.StreamReader sr = new System.IO.StreamReader(
@"AccessToken.txt",
System.Text.Encoding.GetEncoding("utf-8"));
//内容を一行ずつ読み込む
var AccessToken = sr.ReadLine();
//Console.WriteLine(sr.ReadLine());
//閉じる
sr.Close();
var registeredApp = ApplicaionManager.RegistApp("qiitadon.com", "TodoListDon", Scope.Read | Scope.Write | Scope.Follow).Result;
return AccessToken;
}
catch (FileNotFoundException)
{
Console.WriteLine("What is your host?(ex:pawoo.net, qiitadon.com):");
var host = Console.ReadLine();
var registeredApp = ApplicaionManager.RegistApp(host, "TodoListDon", Scope.Read | Scope.Write | Scope.Follow).Result;
var url = ApplicaionManager.GetOAuthUrl(registeredApp);
Console.WriteLine(url);
Console.Write("please copy and peaste AccessToken:");
var code = Console.ReadLine();
var tokens = ApplicaionManager.GetAccessTokenByCode(registeredApp, code).Result;
//ファイルを上書きし、utf-8で書き込む
System.IO.StreamWriter sw = new System.IO.StreamWriter(
@"AccessToken.txt",
false,
System.Text.Encoding.GetEncoding("utf-8"));
//txtの内容を1行ずつ書き込む
sw.WriteLine(tokens.AccessToken);
//閉じる
sw.Close();
return OAuth(p);
}
}
認証を指せるコードをまとめて、アクセストークンをテキストに記述するようにしました。これをしたのには理由が2つあります。
- 記事を書く際に、アクセストークンを晒してしまう
- テキストなら、変数で変動することがないので、いちいち認証をする必要がない
ただし、セキュリティの観点で、よろしくないと思われますので、分かってると思いますが、このアクセストークンが入ったテキストファイルの中身を誰かに渡すのは120%騙されてますのでやめてくださいね。
もし、セキュアな方法をご存知の方いらっしゃったら、ご指摘いただけると幸いです。
Tootモード
APIのほぼ説明通りですが、TodoListを一覧表示させたいので、少し手を加えてあります。手を加えるのが少しでホント助かりますね
static void Toot(string url, string AccessToken, Program p)
{
var client = new MastodonClient(url, AccessToken);
string paste = "";
int i = 0;
foreach (var s in p.TodoList)
{
i++;
paste += i + ":[ ]" + s + Environment.NewLine;
}
client.PostNewStatus(status: paste + "#On_TodoListDon試験運用中");
}
foreachをつかって、TodoListをすべて一つの変数Pasteに格納してから、client.PostNewStatusを使ってトゥートを行っています。こちらは、手動でトゥートを行っています。そこで、時間が来たら自動でトゥートを行うモードを実装します。
自動トゥートモード
実はここの実装に一番苦労しました。
public void AutoToot(int interval, Program p, int boot_time)
{
TaskFactory taskFactory = new TaskFactory();
CancellationTokenSource cancellationToken = new CancellationTokenSource();
Task task = taskFactory.StartNew(() => {
while (true)
{
cancellationToken.Token.ThrowIfCancellationRequested();
DateTime t = DateTime.Now;
int now_m = t.Minute;
now_m = t.Minute;
if (now_m == boot_time + interval)
{
var token = OAuth(p);
Toot("qiitadon.com", token, p);
interval *= 2;
if(interval > 60){
interval %= 60;
}
AutoToot(interval, p, boot_time);
}
Thread.Sleep(500);
}
}, cancellationToken.Token);
Console.ReadLine();
try
{
// キャンセル要求出す
cancellationToken.Cancel();
// タスクがキャンセルされるまで待機
task.Wait();
}
catch (AggregateException)
{
// タスクがキャンセルされるとここが実行される
Console.WriteLine("Task is cancelled.");
}
}
何に苦労したのか。
- クラスしか勉強してない状態で、Taskをやることになった点
- キャンセルのコマンド実装しないと、停止できない
クラスしか勉強してない状態で、Taskをやることになった点
Taskは独習C#でも後半の範囲です。そのため、実装するにも前提知識はクラスしかないので、いきなりの理解は無理でした。このコードはほとんど、このC#のタスクキャンセルのコピペですが、どう値を渡されるのかという話や、「処理を渡す」という基礎を学ぶことができただけでも収穫です。
### キャンセルのコマンド実装しないと、停止できない
このコマンド、死ぬほどではありませんが、危険です。というのも、停止できません。Ctrl+cでもやらない限り止まりませんでした・・・
そこで、先ほどのページを参考にして、実装を行いました。
実装まとめ
では、これらの設計方針をコーディングしていきましょう。
using System;
using System.Collections.Generic;
using Mastodot.Enums;
using Mastodot.Utils;
using Mastodot;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
namespace TodoListDon
{
class Program
{
public List<string> TodoList = new List<string>();
static void Main(string[] args)
{
int id = 0;
var p = new Program();
var edit = new TodoEditMode(id, p, p.TodoList);
p.ModeSelect(id, p, edit);
}
public bool Conform()
{
Console.WriteLine("Continue?");
var answer = Console.ReadLine();
if (answer == "y")
{
return true;
}
else
{
return false;
}
}
static void Toot(string url, string AccessToken, Program p)
{
var client = new MastodonClient(url, AccessToken);
string paste = "";
int i = 0;
foreach (var s in p.TodoList)
{
i++;
paste += i + ":[ ]" + s + Environment.NewLine;
}
client.PostNewStatus(status: paste + "#On_TodoListDon試験運用中");
}
static void Toot(string url, string AccessToken, Program p, int id)
{
var client = new MastodonClient(url, AccessToken);
string paste = "時間です"+Environment.NewLine + id + ":[ ]" + p.TodoList[id-1] + Environment.NewLine;
client.PostNewStatus(status: paste + "#On_TodoListDon試験運用中");
}
static void Save(Program p){
//ファイルを上書きし、Shift JISで書き込む
System.IO.StreamWriter sw = new System.IO.StreamWriter(
@"TodoList.txt",
false,
System.Text.Encoding.GetEncoding("utf-8"));
//TextBox1.Textの内容を1行ずつ書き込む
foreach (string line in p.TodoList)
{
sw.WriteLine(line);
}
//閉じる
sw.Close();
}
static void Road(Program p){
//"C:\test\1.txt"をShift-JISコードとして開く
System.IO.StreamReader sr = new System.IO.StreamReader(
@"TodoList.txt",
System.Text.Encoding.GetEncoding("utf-8"));
//内容を一行ずつ読み込む
while (sr.Peek() > -1)
{
p.TodoList.Add(sr.ReadLine());
//Console.WriteLine(sr.ReadLine());
}
//閉じる
sr.Close();
}
static string OAuth(Program p){
try
{
//"C:\test\1.txt"をShift-JISコードとして開く
System.IO.StreamReader sr = new System.IO.StreamReader(
@"AccessToken.txt",
System.Text.Encoding.GetEncoding("utf-8"));
//内容を一行ずつ読み込む
var AccessToken = sr.ReadLine();
//Console.WriteLine(sr.ReadLine());
//閉じる
sr.Close();
var registeredApp = ApplicaionManager.RegistApp("qiitadon.com", "TodoListDon", Scope.Read | Scope.Write | Scope.Follow).Result;
return AccessToken;
}
catch (FileNotFoundException)
{
Console.WriteLine("What is your host?(ex:pawoo.net, qiitadon.com):");
var host = Console.ReadLine();
var registeredApp = ApplicaionManager.RegistApp(host, "TodoListDon", Scope.Read | Scope.Write | Scope.Follow).Result;
var url = ApplicaionManager.GetOAuthUrl(registeredApp);
Console.WriteLine(url);
Console.Write("please copy and peaste AccessToken:");
var code = Console.ReadLine();
var tokens = ApplicaionManager.GetAccessTokenByCode(registeredApp, code).Result;
//ファイルを上書きし、Shift JISで書き込む
System.IO.StreamWriter sw = new System.IO.StreamWriter(
@"AccessToken.txt",
false,
System.Text.Encoding.GetEncoding("utf-8"));
//TextBox1.Textの内容を1行ずつ書き込む
sw.WriteLine(tokens.AccessToken);
//閉じる
sw.Close();
return OAuth(p);
}
}
public void AutoToot(int interval, Program p, int boot_time)
{
TaskFactory taskFactory = new TaskFactory();
CancellationTokenSource cancellationToken = new CancellationTokenSource();
Task task = taskFactory.StartNew(() => {
while (true)
{
cancellationToken.Token.ThrowIfCancellationRequested();
DateTime t = DateTime.Now;
int now_m = t.Minute;
now_m = t.Minute;
if (now_m == boot_time + interval)
{
var token = OAuth(p);
Toot("qiitadon.com", token, p);
interval *= 2;
if(interval > 60){
interval %= 60;
}
AutoToot(interval, p, boot_time);
}
Thread.Sleep(500);
}
}, cancellationToken.Token);
Console.ReadLine();
try
{
// キャンセル要求出す
cancellationToken.Cancel();
// タスクがキャンセルされるまで待機
task.Wait();
}
catch (AggregateException)
{
// タスクがキャンセルされるとここが実行される
Console.WriteLine("Task is cancelled.");
}
}
public void Alerm(int interval, Program p, int boot_time,int id){
TaskFactory taskFactory = new TaskFactory();
CancellationTokenSource cancellationToken = new CancellationTokenSource();
Task task = taskFactory.StartNew(() => {
while (true)
{
cancellationToken.Token.ThrowIfCancellationRequested();
DateTime t = DateTime.Now;
int now_m = t.Minute;
now_m = t.Minute;
if (now_m == boot_time + interval)
{
var token = OAuth(p);
Toot("qiitadon.com", token, p, id);
interval *= 2;
AutoToot(interval, p, boot_time);
}
Thread.Sleep(500);
}
}, cancellationToken.Token);
Console.ReadLine();
try
{
// キャンセル要求出す
cancellationToken.Cancel();
// タスクがキャンセルされるまで待機
task.Wait();
}
catch (AggregateException)
{
// タスクがキャンセルされるとここが実行される
Console.WriteLine("Task is cancelled.");
}
}
public void ModeSelect(int id, Program p, TodoEditMode edit){
Console.Write(
@"Please select Mode
(Add mode:a, Insert_Mode:i, TootMode(Manual):t, View_Mode:v,
exit:e, save:s, road:r, delete:d AutoTootMode:at):");
var Mode = Console.ReadLine();
switch (Mode)
{
case "a":
id++;
edit.Adding(id, p);
break;
case "i":
edit.Insert(p);
break;
case "v":
edit.View(p);
ModeSelect(id, p, edit);
break;
case "e":
return;
case "t":
var token = OAuth(p);
Toot("qiitadon.com", token, p);
ModeSelect(id, p, edit);
break;
case "s":
Save(p);
ModeSelect(id, p, edit);
break;
case "r":
Road(p);
ModeSelect(id, p, edit);
break;
case "d":
edit.View(p);
Console.WriteLine("Please Write id to delete");
int i = int.Parse(Console.ReadLine());
edit.Delete(i, p, edit);
ModeSelect(id, p, edit);
break;
case "at":
var t = DateTime.Now;
Console.Write("Please Write Interval Minit to Toot Todo:");
int interval = int.Parse(Console.ReadLine());
Console.WriteLine("Press to Quit a key");
int boot_time = t.Minute;
AutoToot(interval, p, boot_time);
ModeSelect(id,p,edit);
break;
case "al":
t = DateTime.Now;
Console.Write("Please Write Interval Minit to Toot Todo:");
interval = int.Parse(Console.ReadLine());
edit.View(p);
Console.WriteLine("Please write ID to Toot Alerm");
id = int.Parse(Console.ReadLine());
Console.WriteLine("Press to Quit a key");
boot_time = t.Minute;
Alerm(interval, p, boot_time,id);
ModeSelect(id, p, edit);
break;
}
}
}
}
最終的にチュートリアルになったのか
チュートリアルでのそれぞれの学習目的は、「APIに触れて、繋げてみることで、クライアントアプリケーションを作ること」「GUIの実装」であるため、結果的にはチュートリアルになったのか甚だ疑問ですね・・・
GUIの実装はこれをベースにして出来るのではないかとは考えています。
作り上げてからかなりの時間が立ってしまいましたがこれからも、勉強して作りたいものを作っていこうと思います。