lelegram: init

This is an IRC/Telegram bridge.

It does multi-account puppet-like access to IRC making everyone's life
easier.

Compared to teleirc it also:
 - is smarter about converting messages
 - uses teleimg for public image access
 - is not written in JS

Experimental for now.

Change-Id: I66ba3f83abdfdea6463ab3be5380d8d3f2769291
diff --git a/personal/q3k/lelegram/telegram.go b/personal/q3k/lelegram/telegram.go
new file mode 100644
index 0000000..a80e76e
--- /dev/null
+++ b/personal/q3k/lelegram/telegram.go
@@ -0,0 +1,169 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+	"github.com/golang/glog"
+)
+
+// telegramConnection runs a long-lived connection to the Telegram API to receive
+// updates and pipe resulting messages into telLog.
+func (s *server) telegramConnection(ctx context.Context) error {
+	u := tgbotapi.NewUpdate(0)
+	// TODO(q3k): figure out what the _fuck_ does this even mean
+	u.Timeout = 60
+
+	updates, err := s.tel.GetUpdatesChan(u)
+	if err != nil {
+		return fmt.Errorf("GetUpdatesChan(%+v): %v", u, err)
+	}
+
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case u, ok := <-updates:
+			if !ok {
+				return fmt.Errorf("Updates channel closed")
+			}
+
+			// Dispatch update.
+			switch {
+			case u.Message != nil:
+				if u.Message.Chat.ID != s.groupId {
+					glog.Infof("[ignored group %d] <%s> %v", u.Message.Chat.ID, u.Message.From, u.Message.Text)
+					continue
+				}
+				if msg := plainFromTelegram(s.tel.Self.ID, &u); msg != nil {
+					s.telLog <- msg
+				}
+			}
+		}
+	}
+}
+
+// telegramLoop maintains a telegramConnection.
+func (s *server) telegramLoop(ctx context.Context) {
+	for {
+		err := s.telegramConnection(ctx)
+		if err == ctx.Err() {
+			glog.Infof("Telegram connection closing: %v", err)
+			return
+		}
+
+		glog.Errorf("Telegram connection error: %v", err)
+		select {
+		case <-ctx.Done():
+			return
+		case <-time.After(1 * time.Second):
+			continue
+		}
+	}
+}
+
+// plainFromTelegram turns a Telegram message into a plain text message.
+func plainFromTelegram(selfID int, u *tgbotapi.Update) *telegramPlain {
+	parts := []string{}
+
+	from := u.Message.From
+	replyto := u.Message.ReplyToMessage
+	text := u.Message.Text
+
+	// This message is in reply to someone.
+	if replyto != nil && text != "" && replyto.From != nil {
+		// The rendered name of the author of the quote.
+		ruid := "@" + replyto.From.String()
+
+		// First line of the quoted text.
+		quotedLine := ""
+
+		// Check if the quoted message is from our bridge.
+		if replyto.From.ID == selfID {
+			// Someone replied to an IRC bridge message, extract nick and line from there
+			// eg: "<q3k> foo bar baz" -> ruid = q3k; quotedLine = foo bar baz
+			t := replyto.Text
+			if strings.HasPrefix(t, "<") {
+				p := strings.SplitN(t[1:], ">", 2)
+				nick := p[0]
+				quoted := strings.TrimSpace(p[1])
+
+				// ensure nick looks sane
+				if len(nick) < 16 && len(strings.Fields(nick)) == 1 {
+					quotedLine = strings.TrimSpace(strings.Split(quoted, "\n")[0])
+					ruid = nick
+				}
+			}
+		} else {
+			// Someone replied to a native telegram message.
+			quoted := strings.TrimSpace(replyto.Text)
+			quotedLine = strings.TrimSpace(strings.Split(quoted, "\n")[0])
+		}
+
+		// If we have a line, quote it. Otherwise just refer to the nick without a quote.
+		if quotedLine != "" {
+			parts = append(parts, fmt.Sprintf("%s: >%s\n", ruid, quotedLine))
+		} else {
+			parts = append(parts, fmt.Sprintf("%s: ", ruid))
+		}
+	}
+
+	// This message contains a sticker.
+	if u.Message.Sticker != nil {
+		emoji := ""
+		if u.Message.Sticker.SetName != "" {
+			emoji += "/" + u.Message.Sticker.SetName
+		}
+		if u.Message.Sticker.Emoji != "" {
+			emoji += "/" + u.Message.Sticker.Emoji
+		}
+		parts = append(parts, fmt.Sprintf("<sticker%s>", emoji))
+	}
+
+	// This message contains an animation.
+	if u.Message.Animation != nil {
+		a := u.Message.Animation
+		parts = append(parts, fmt.Sprintf("<uploaded animation: %s >\n", fileURL(a.FileID, "mp4")))
+	}
+
+	// This message contains a document.
+	if u.Message.Document != nil {
+		d := u.Message.Document
+		fnp := strings.Split(d.FileName, ".")
+		ext := "bin"
+		if len(fnp) > 1 {
+			ext = fnp[len(fnp)-1]
+		}
+		parts = append(parts, fmt.Sprintf("<uploaded file: %s >\n", fileURL(d.FileID, ext)))
+	}
+
+	// This message contains a photo.
+	if u.Message.Photo != nil {
+		// Multiple entries are for different file sizes, choose the highest quality one.
+		hq := (*u.Message.Photo)[0]
+		for _, p := range *u.Message.Photo {
+			if p.FileSize > hq.FileSize {
+				hq = p
+			}
+		}
+		parts = append(parts, fmt.Sprintf("<uploaded photo: %s >\n", fileURL(hq.FileID, "jpg")))
+	}
+
+	// This message has some plain text.
+	if text != "" {
+		parts = append(parts, text)
+	}
+
+	// Was there anything that we extracted?
+	if len(parts) > 0 {
+		return &telegramPlain{from.String(), strings.Join(parts, " ")}
+	}
+	return nil
+}
+
+func fileURL(fid, ext string) string {
+	return flagTeleimgRoot + fid + "." + ext
+}