How to spend the terminal

技術ブログでさえない

Goで作ったprintfでHello world

この記事は、SLP KBIT Advent Calendar 2017 12日目の記事です。

はじめに

予告していた通りGo言語でHello worldを書きます。 なぜHello worldを書くのかというと、アドベントカレンダーの記事の水準が上がってきていて、初心者が書きにくくなってきているからです。Hello worldを書いた程度の微笑ましい記事でもいいと私は思っています。

package main
import "fmt"
func main() {
    fmt.Printf("Hello world\n")
}

はい、Hello worldを書けました。これで終わりです・・・と本当に言ったら怒られるので本題に入ります。

printfを作る

*nixのシェルにはprintfコマンドが組み込まれています。printfコマンドはC言語のprintfのように引数を書式にしたがって変換し、出力します。

これを作ろうと思った理由は二つあって、Windowsで*nixシェルっぽいコマンドを作っていたのと、楽に作れそうだからです。 楽に作れそうだったのですが、なかなか大変でした。

fmt.Printfに投げて終わり?

はじめに考えていたのは次のようなプログラムでした。

package main

import (
    "fmt"
    "os"
)

func main() {
      if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", os.Args[0])
    } else if len(os.Args) == 2 {
        f := os.Args[1]
        fmt.Printf(f)
    } else {
        f := os.Args[1]
        s := os.Args[2:]
        fmt.Printf(f, s...)  // スライスを可変引数にして渡す
    }
}

os.Args1がフォーマット(書式)で、os.Args2:が引数となっています。 このまま実行できればそれで終わりだったのですが、数値の変換が行われません。 なぜそうなるかというと、os.Argsがstringの配列で、stringとして渡されるからです。

引数を変換する

fmt.Printf()の引数をGoDocで見てみましょう。

func Printf(format string, a ...interface{}) (n int, err error)

1番目の引数は書式で文字列、2番目以降の引数はinterface{}(任意の型)の可変引数です。つまり、2番目以降の引数を変換したものを[]interface{}(interface{}の配列)に入れておけばいいわけです。

では、どうやって変換すればいいでしょうか。例えば、99が整数であるか文字列であるか判断することはできません。そこでformatを参照して文字列であるか整数であるか実数であるかを判断します。

判断する関数を作る

使える書式

今回のprintfで使える書式は以下の書式です。 - 文字列 - %s - %q(ダブルクォーテーションをつける) - 整数 - %d - %o - %x - %X - 実数 - %f - %F - %e - %E - その他 - %%(%を出力)

どう解析するか

1文字ずつ調べて、'%'を見つけたら書式フラグを立てます。 書式であることがわかれば文字列であれば文字列、整数であれば整数、実数であれば実数を配列に加えます。整数か実数が指定されているのにそうでない場合は0もしくは0.0を配列に加えます。 書式でない文字が出てきた場合か書式フラグが立ったまま解析が終わった場合、エラーを返します。

コード

func StrToIF(f string, s []string) ([]interface{}, error) {
    a := []interface{}{}
    runes := []rune(f)
    argNum := 0
    percent := false
    verb := ""
    for _, r := range runes {
        if percent {
            verb += string(r)
            switch r {
            case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
            case '+', '-', '.', '#':
            case '%':
                if verb != "%%" {
                    return a, FormatError{verb}
                } else { // %%
                    percent = false
                    continue
                }
            case 's', 'q':
                if argNum < len(s) {
                    a = append(a, s[argNum])
                    argNum++
                } else {
                    a = append(a, "")
                }
                percent = false
            case 'd', 'o', 'x', 'X':
                var i int64
                var err error
                if argNum < len(s) {
                    i, err = strconv.ParseInt(s[argNum], 0, 64)
                    if err != nil {
                        i = 0
                    }
                    argNum++
                } else {
                    i = 0
                }
                a = append(a, i)
                percent = false
            case 'f', 'F', 'e', 'E':
                var fl float64
                var err error
                if argNum < len(s) {
                    fl, err = strconv.ParseFloat(s[argNum], 64)
                    if err != nil {
                        fl = 0.0
                    }
                    argNum++
                } else {
                    fl = 0.0
                }
                a = append(a, fl)
                percent = false
            default:
                return a, FormatError{verb}
            }
        }
        if r == '%' {
            if percent == false {
                percent = true
                verb = "%"
            }
        }
    }
    if percent {
        return a, FormatError{verb}
    }
    return a, nil
}

エスケープシーケンスの処理

書式に合わせた変換はできるようになりましたが、"Hello world\n"を入力すると開業されずにそのまま出力されます。これは"[\n]"が"[][n]"といったようになるからです。 そのため、エスケープシーケンスに変換します。stringsパッケージを用いて泥臭く実装しました。

func formatReplace(f string) string {
    f = strings.Replace(f, "\\a", "\a", -1)
    f = strings.Replace(f, "\\b", "\b", -1)
    f = strings.Replace(f, "\\f", "\f", -1)
    f = strings.Replace(f, "\\n", "\n", -1)
    f = strings.Replace(f, "\\r", "\r", -1)
    f = strings.Replace(f, "\\t", "\t", -1)
    f = strings.Replace(f, "\\v", "\v", -1)
    f = strings.Replace(f, "\\'", "'", -1)
    f = strings.Replace(f, "\\\"", "\"", -1)
    return f
}

最後に加工した書式と[]interface{}を可変引数に展開してfmt.Printf()に投げて完成です。

まとめ

以上よりprintfを作ることができました。ソースコードGitHubに上げています。

github.com

標準ライブラリのみで使ったのでWindowsでも簡単に使うことができます。 これでprintfを行うことで誰でもシェル上でHello worldを行うことができます。 シェルならWindowsでもechoコマンドでできますが。

全国で使える雨雲接近検知システム

通り雨という洗濯物の乾きを奪う恐ろしい存在があります。今まで何度も洗濯物をやられました。 そのような悲しみをこれ以上しないために雨雲が接近したら通知するシステムを作ろうと思いました。外出中だとあまり意味ないですけど

DIAS

DIAS(Data Integration and Analysis System)という主に東京大学が実施しているシステムがあります。DIASでは地球環境の観測を行なっており、観測データを提供しています。ここで雨雲データを入手すれば容易に作れそうですが、思わぬ問題がありました。

それはユーザ登録ページがHTTPだということです。

さすがにログインページはHTTPSですが、パスワードと名前と所属組織名と電話番号を平文で送信しないといけないのはまずいでしょう。パスワードはともかく、名前と所属組織名と電話番号は嘘をつくことができないので盗聴されていたら心配です。(やましい理由がなくても) そこでデータをどこかのサイトから入手する必要があります。

データを入手するサイト

東京は東京アメッシュ(http://tokyo-ame.jwa.or.jp/)を利用することができます。東京アメッシュで入手できる雨雲メッシュの画像は透過GIFなので加工が非常に楽です。しかし、東京アメッシュで入手できるデータは東京に限定されています。入手したいデータは香川県高松市なので他のサイトを利用する必要があります。そこでXRAIN(http://www.river.go.jp/x/xmn0107010.php)を利用します。

使用言語

使用した言語はGo言語です。やっていることは単純なので、無理にGo言語で書くよりはPythonなどで書いた方がいいと思います。というかPythonの方が楽でしょう。

プログラム

プログラムはGitHubに公開しています。

go get github.com/moxtsuan/murakumo

で入手することもできます。

github.com

ss.goはスクリーンショットの撮影、cutter.goは画像のトリミング、colorcount.goは雨雲メッシュを測定します。 var.goの値を測定したい地域になるよう変更してください。lonは経度、latは緯度、opaは透明度、zoomは縮尺です。

画像の入手

XRAINではcanvasJavaScriptで描画されています。そのため、普通のスクレイピングでは入手することができません。JavaScriptを実行させて描画させた上で入手する必要があります。 今回はcanvasをそのまま入手することができなかったので、スクリーンショットを撮影して画像を入手します。使用したブラウザはPhantomJSで、agouti(https://github.com/sclevine/agouti)を用いて操作を行います。入手した画像は次のようになります。

f:id:moxtsuan:20170914201458p:plain:w300

画像のトリミング

撮影した画像のトリミングを行います。トリミングにはcutter(https://github.com/oliamb/cutter)を使用しました。トリミングしたサイズは480x480で、zoom=12だと約15kmになるはずです。

f:id:moxtsuan:20170914201737p:plain:w300

雨雲メッシュの色を数える

雨雲メッシュの色を数えます。あらかじめ降水量ごとの色を設定しておき、それと一致すればカウントするようにしています。降水量が多いほど重要度が高いので降水量によってカウントする値を変えています。 降水量ごとの色は環境によって違うことがあるので適宜変更してください。一致なので色と色の間などが正しく測定できません。近似していればカウントする方が正しく測定することができると思います。

(2017/09/16 追記)

近似する色も測定できるようにしました。

検知

検知は測定値が閾値を超えたら行います。 終了ステータスをシェルスクリプトなどで拾って通知などを行います。

(2017/09/15 追記)

cronで自動実行する場合はPhantomJSのPATHをcronに記述してください。そうしないと動きません。 終了ステータスを1にしているとlog.Fatal()と被って誤動作を起こすので2にしました。

まとめ

全国で使える雨雲接近検知システムを作ることができました。 XRAINはこんな利用方法は想定されていませんので、入手しにくいデータであることはやむを得ないでしょう。 毎分取得は負荷がかかりそうなので負荷がかからない程度に運用しようと思っています。

東京大学などは早くDIASの登録ページをHTTPSにしてください。(重要)

Go言語でnkfみたいなやつ

nkfという今年で30周年になる古いソフトがあります。nkfでは文字コードを変換することができます。 nkfには様々な機能がありますが、次の3点を満たすものをGo言語で作ってみました。

文字コードを変換する

文字コードの変換は以下の記事を参考にしました。というか丸コピ

qiita.com

文字コードを判定する

文字コードの判定はchardetを使用しました。chardetは文字コードを判定するためのライブラリです。 判定に失敗した時にはエラーを返します。

改行コードを変換する

改行コードの変換は正規表現を使用しました。

コード

コードは以下のようになりました。

main.go

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
)

var (
    inJIS     = flag.Bool("J", false, "Input JIS(ISO2022JP)")
    inEUC     = flag.Bool("E", false, "Input EUCJP")
    inSJIS    = flag.Bool("S", false, "Input Shift_JIS")
    inUTF8    = flag.Bool("W", false, "Input UTF-8")
    outJIS    = flag.Bool("j", false, "Output JIS(ISO2022JP)")
    outEUC    = flag.Bool("e", false, "Output EUCJP")
    outSJIS   = flag.Bool("s", false, "Output Shift_JIS")
    outUTF8   = flag.Bool("w", false, "Output UTF-8")
    unix      = flag.Bool("Lu", false, "LF(UNIX) Newline")
    windows   = flag.Bool("Lw", false, "CRLF(Windows) Newline")
    macintosh = flag.Bool("Lm", false, "CR(Macintosh) Newline")
    override  = flag.Bool("override", false, "Override")
    guess     = flag.Bool("g", false, "Detect Charset")
)

func setIn() string {
    switch {
    case *inJIS:
        return "ISO2022JP"
    case *inEUC:
        return "EUCJP"
    case *inSJIS:
        return "ShiftJIS"
    case *inUTF8:
        return "UTF8"
    default:
        return ""
    }
}

func setOut() string {
    switch {
    case *outJIS:
        return "ISO2022JP"
    case *outEUC:
        return "EUCJP"
    case *outSJIS:
        return "ShiftJIS"
    case *outUTF8:
        return "UTF8"
    default:
        return "ISO2022JP"
    }
}

func setNl() string {
    switch {
    case *unix:
        return "UNIX"
    case *windows:
        return "WINDOWS"
    case *macintosh:
        return "MACINTOSH"
    default:
        return ""
    }
}

func main() {
    var fp *os.File
    var err error
    flag.Parse()
    if len(flag.Args()) < 1 {
        fp = os.Stdin
    } else {
        fp, err = os.Open(flag.Args()[0])
        if err != nil {
            log.Fatal(err)
        }
        defer fp.Close()
    }

    in := setIn()
    out := setOut()
    nl := setNl()
    if *guess {
        charset, _ := Guess(fp)
        fmt.Println(charset)
    } else {
        str, err := Convert(fp, in, out, nl)
        if err != nil {
            log.Fatal(err)
        }
        if *override && !(len(flag.Args()) < 1) {
            fp.Close()
            fp, err = os.OpenFile(flag.Args()[0], os.O_WRONLY, 0666)
            if err != nil {
                log.Fatal(err)
            }
            defer fp.Close()
            fmt.Fprint(fp, str)
        } else {
            fmt.Fprint(os.Stdout, str)
        }
    }
}

convert.go

package main

import (
    "errors"
    "io/ioutil"
    "os"
    "regexp"
    "strings"

    "github.com/saintfish/chardet"
    "golang.org/x/text/encoding/japanese"
    "golang.org/x/text/transform"
)

var errDet = errors.New("Couldn't detect")

func charDet(b []byte) (string, error) {
    d := chardet.NewTextDetector()
    res, err := d.DetectBest(b)
    if err != nil {
        return "", err
    }
    switch res.Charset {
    case "Shift_JIS":
        return "ShiftJIS", nil
    case "UTF-8":
        return "UTF8", nil
    case "EUC-JP":
        return "EUCJP", nil
    case "ISO-2022-JP":
        return "ISO2022JP", nil
    default:
        return res.Charset, errDet
    }
}

func toUtf8(str string, in string) (string, error) {
    var u8 []byte
    var err error
    switch in {
    case "ISO2022JP":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ISO2022JP.NewDecoder()))
    case "EUCJP":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.EUCJP.NewDecoder()))
    case "ShiftJIS":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewDecoder()))
    case "UTF8":
        u8, err = []byte(str), nil
    default:
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewDecoder()))
    }
    if err != nil {
        return "", err
    }
    return string(u8), err
}

func toSjis(str string) (string, error) {
    sjis, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(sjis), err
}

func toEuc(str string) (string, error) {
    euc, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.EUCJP.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(euc), err
}

func toJis(str string) (string, error) {
    jis, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ISO2022JP.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(jis), err
}

func nlRep(str string, nl string) string {
    rep := regexp.MustCompile(`\r\n|\r|\n`)
    switch nl {
    case "UNIX":
        return rep.ReplaceAllString(str, "\n")
    case "WINDOWS":
        return rep.ReplaceAllString(str, "\r\n")
    case "MACINTOSH":
        return rep.ReplaceAllString(str, "\r")
    }
    return str
}

/*
Only detect Character encoding
*/
func Guess(file *os.File) (string, error) {
    input, err := ioutil.ReadAll(file)
    if err != nil {
        return "", err
    }
    det, err := charDet(input)
    return det, err
}

/*
Convert Character encoding
*/
func Convert(file *os.File, in string, out string, nl string) (string, error) {
    input, err := ioutil.ReadAll(file)
    if err != nil {
        return "", err
    }
    if in == "" {
        in, err = charDet(input)
        if err != nil {
            return "", err
        }
    }
    u8, err := toUtf8(string(input), in)
    if err != nil {
        return "", err
    }
    if nl != "" {
        u8 = nlRep(u8, nl)
    }
    var output string
    switch out {
    case "ISO2022JP":
        output, err = toJis(u8)
    case "ShiftJIS":
        output, err = toSjis(u8)
    case "EUCJP":
        output, err = toEuc(u8)
    case "UTF8":
        output = u8
    }
    return output, err
}

まとめ

EUC-JPの判定はあまりうまくいかないのでEオプションをつけるといいと思います。 chardetはShift_JISEUC-JPで誤判定が発生する可能性があるため、overrideオプションを使う時は入力オプションを使うことをおすすめします。 ここまで長くなるとコードをまるまる載せるのではなく、GitHubで公開したほうがいいと思いました。 GitHubに公開しました(2017/07/14)

github.com

Windowsでuptimeコマンドもどき(Go言語)

Windowsで稼働時間を確認する方法としてコマンドプロンプトでの"net statistics server"やタスクマネージャなどがあります。
しかし、"net statistics server"で得られる情報は開始日時で稼働時間を得るには計算がいりますし、タスクマネージャは起動しなければいけません。
そのためにコマンドラインで稼働時間を取得するためにGo言語でuptimeもどきを作りました。ロードアベレージを取得するには前もって用意していないと駄目で、ユーザ数を取得するWin32 APIは知らないので現在時刻と稼働時間のみ出力します。

package main

import (
	"fmt"
	"log"
	"os"
	"syscall"
	"time"
)

func main() {
	dll, err := syscall.LoadDLL("Kernel32.dll")
	if err != nil {
		log.Fatal(err)
	}
	defer dll.Release()

	proc, err := dll.FindProc("GetTickCount64")
	if err != nil {
		log.Fatal(err)
	}

	tcP, _, lastErr := proc.Call()
	lastErr = syscall.GetLastError()
	if lastErr != nil {
		log.Fatal(lastErr)
	}
	tc := (uint64)(tcP)
	sec := tc / 1000
	min := sec / 60
	hour := min / 60
	day := hour / 24
	hour %= 24
	min %= 60
	var up string
	if day > 0 {
		if day > 1 {
			up = fmt.Sprintf("up %d days, %02d:%02d", day, hour, min)
		} else {
			up = fmt.Sprintf("up %d day, %02d:%02d", day, hour, min)
		}
	} else if hour > 0 {
		up = fmt.Sprintf("up  %02d:%02d", hour, min)
	} else {
		up = fmt.Sprintf("up  %d min", min)
	}

	t := time.Now()
	now := fmt.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second())

	fmt.Fprintf(os.Stdout, " %s %s\n", now, up)
}

実行結果は次のようになります。XPの場合、GetTickCount64がないのでGetTickCountを使ってtcのキャストをuint32にしてください。
f:id:moxtsuan:20170630134706p:plain

長い時間稼働させると何が起こるかわからないので定期的に再起動は行いましょう。

Go言語でriver.go.jpからダムの貯水率を取得する

最近雨が降らないな、と思っていたらニュースで早明浦ダム取水制限が始まることを知り驚きました。 取水制限が始まるくらい貯水率が減っていたのなら早明浦ダムbotが毎時間ツイートしているだろう、 と思っていたら止まっていました。(2017/06/18現在動いています。)

作者の 103yen (@103yen) | Twitter さんによると、 river.go.jpの仕様が変わって貯水率を取得できなかったようです。

なんでbotが止まっていたかどうかわからなかったので、自分で貯水率を取得してつぶやこうと思いました。 作者さんのWebページ( ダムbotシリーズについて )とブログ( Pythonでriver.go.jpからダムの貯水率を取得する - 99円のへたれ日記! )を参考にしてGo言語で実装しました。(ほぼgoquery)

package main

import (
    "fmt"
    "log"

    "github.com/PuerkitoBio/goquery"
)

type Storage struct {
    date string
    time string
    pos  string
}

var (
    BASE_URL           = "http://www1.river.go.jp"
    OUTSIDE_URL_BEFORE = "/cgi-bin/DspDamData.exe?ID="
    OUTSIDE_URL_AFTER  = "&KIND=3&PAGE=0"
    ID                 = "1368080700010"
)

func GetPerOfStorage() (s Storage) {
    found := false
    outside_url := BASE_URL + OUTSIDE_URL_BEFORE + ID + OUTSIDE_URL_AFTER
    outside, err := goquery.NewDocument(outside_url)
    if err != nil {
        log.Fatal(err)
    }
    iframe := outside.Find("iframe")
    iframe_url, _ := iframe.Attr("src")
    contents_url := BASE_URL + iframe_url
    contents, _ := goquery.NewDocument(contents_url)
    contents.Find("tr").EachWithBreak(func(i int, tr *goquery.Selection) bool {
        if found == true {
            return false
        }
        tr.Find("td").EachWithBreak(func(j int, td *goquery.Selection) bool {
            if j == 0 {
                s.date = td.Text()
            } else if j == 1 {
                s.time = td.Text()
            } else if j == 6 {
                s.pos = td.Text()
            }
            if s.pos != "" && s.pos != "-" {
                found = true
                return false
            }
            return true
        })
        return true
    })
    return
}

func main() {
    s := GetPerOfStorage()
    fmt.Printf("%s %s %s\n", s.date, s.time, s.pos)
}

これで取得したデータをRubyを用いてツイートしました。

2時間ごとにツイートしていたのですが、早明浦ダムbotが復活したのでtwitterでのツイートはやめました。 (GNU socialでは毎時間クイップしてます)

Raspberry Pi 2 Model BでGNU social

この記事は、SLP KBIT Advent Calendar 2016 10日目の記事です。

はじめに

Twitterはことあるごとにダウンします。 自分はツイートをよくするのでじれったく思います。 Twitterは最近身売りを検討していると聞きます。 買収されるならともかく、もしTwitterがなくなったらどうすればいいでしょうか。

Twitterが使えない時にどのSNSを使えばいいでしょうか、FB、Instagram、LINEなど様々なSNSがありますが、Twitterの完全な代替となるようなSNSはほとんどないと思います。 しかし、その中でもGNU socialはTwitterの代わりになりうると思いました。

GNU social

GNU socialはTwitterのようなマイクロブログを提供するソフトウェアです。 名前の通りフリーソフトです。 GNU socialの目的はマイクロブログの連合を作ることによる中央集権化された資本主義なサービスからの離脱、とのことです。 ソースコードgithubで公開されているため、簡単に導入することができます。PHPで書かれているのでビルドの必要はありません。

導入した理由

以前より、 https://Quitter.no でアカウントを作ってクイップを行っていたのですが、GNU socialの思想が(フリーソフトウェアという思想の他に)自分のサーバに掲示板を建てたりするパソコン通信と似ているのではと思い、導入しようと思いました。導入の際にApacheとnginxの使い方を学ぶこともできました。

Raspberry Piを使った理由

Raspberry Piで建てられるなら安価でGNU socialを普及することができると思ったからです。RPiより安いマシンがあるかも

今回使ったRaspberry Piは「Raspberry Pi 2 Model B」です。

(2017/04/16 追記)OSはRaspbian Jessieです。

参考サイト

以下のサイトを参考にしました。

GNU socialのインストール - Akionux-wiki

Instalar GNUSocial en una Raspberry Pi - la Enredadera

GNU Social · Mesh network Practical Guide

導入まで

下準備

GNU socialには - PHP - MariaDB - サーバ

の3つが必要なので、 GNU socialのインストール - Akionux-wiki を参考にしてインストールします。

nginxを用いる場合、

sudo apt-get install git nginx php5 mariadb-server mariadb-client php5-curl php5-gd php5-intl php5-gmp php5-json php5-mysqli openssl exif gettext

で揃います。

MariaDBの設定

MariaDBにログインして、データベースを作ります(ここではsocialとします)。

CREATE DATABASE social;
GRANT ALL on social.* TO 'social'@'localhost' IDENTIFIED BY 'agoodpassword'

GNU socialのダウンロード

GNU Socialを適当なディレクトリにダウンロードします。

git clone https://git.gnu.io/gnu/gnu-social.git

ダウンロードしたら公開するディレクトリ(ここでは/var/wwwとします)にgnu-socialを移動して、書き込み権限を与えます。

sudo chmod a+w /var/www/gnu-social/

gnu-socialに - avatar - background - file

というディレクトリを作って書き込み権限を与えます。

nginxの設定

nginxの設定を GNU Social · Mesh network Practical Guide の"Confiugre Nginx Web Server to Server GNU Social"を参考にして行います。

基本的には参考元の設定(server_nameの名前は変える、httpsを使う場合はポート番号を443にしてSSL設定を追加する)を使えばいいのですが、httpでアクセスされた時にhttpsに変換するために次の設定を追加しました。

server {
  listen 80;
  server_name example.com;
  rewrite ^ https://$server_name$request_uri? permanent;
}

アクセス

アクセスしてうまく設定ができていれば初期設定ができます。初期設定ができれば導入完了です。

最後に

2ヶ月ほどの試行錯誤の末、GNU socialを導入することができました。

自分で立てたインスタンスなので、何をやっても自由です。 みなさんも自由な自宅SNSを立ててみてください。

グローバルIPの変更をメールで通知するシェルスクリプト

グローバルIPアドレスは固定サービスを使わない限り変わります。 IPアドレスが変わってしまうといろいろ困るため、メールで通知するようなシェルスクリプトを書きました。 DiCE使えばこんな面倒なことしなくていい

#!/bin/sh -eu

MAIL_TO="TO"
SUBJECT="SUBJECT"

globalip="/var/tmp/globalip" #"XXX.XXX.XXX.XXX"だけ書かれたファイル
tmp=`mktemp`
curl -sS inet-ip.info > ${tmp}
set +e
cmp ${globalip} ${tmp} > /dev/null
res=$?
set -e

ip=`cat ${tmp}`

mail_send () {
echo ${ip} | mail -s "$SUBJECT" "$MAIL_TO"
}

if [ $res -eq 1 ]; then
        mail_send
        cat ${tmp} > ${globalip}
fi

exit 0

このスクリプトの問題点はcurlを使うことで、この手のグローバルIPを教えてくれるサイトはよく落ちます。
そのため、複数の候補から応答するものを使うようにしようと思っています。 また、以前のIPが書いてあるファイルがなければ作るなど改良したい所はいろいろあります。