diff --git a/cmdFollow.go b/cmdFollow.go new file mode 100644 index 0000000..ef2b7e2 --- /dev/null +++ b/cmdFollow.go @@ -0,0 +1,34 @@ +// +build !rm_basic_commands allcommands followcmd + +package main + +import ( + "fmt" +) + +func init() { + command := Command{ + Cmd: []string{"follow"}, + Description: "$username - Follows the given user", + Help: "", + Exec: cmdFollow, + } + RegisterCommand(command) +} + +func cmdFollow(cmd []string) { + if len(cmd) == 2 { + go follow(cmd[1]) + } else { + printFollowHelp() + } +} +func follow(username string) { + k.Exec("follow", username, "-y") + printInfoF("Now follows $TEXT", config.Colors.Message.LinkKeybase.stylize(username)) + followedInSteps[username] = 1 +} + +func printFollowHelp() { + printInfo(fmt.Sprintf("To follow a user use %sfollow ", config.Basics.CmdPrefix)) +} diff --git a/cmdInspect.go b/cmdInspect.go new file mode 100644 index 0000000..5b01c56 --- /dev/null +++ b/cmdInspect.go @@ -0,0 +1,135 @@ +// +build !rm_basic_commands allcommands inspectcmd + +package main + +import ( + "fmt" + "regexp" + "samhofi.us/x/keybase" + "strconv" + "strings" +) + +func init() { + command := Command{ + Cmd: []string{"inspect", "id"}, + Description: "$identifier - shows info about $identifier ($identifier is either username, messageId or team)", + Help: "", + Exec: cmdInspect, + } + + RegisterCommand(command) +} + +func cmdInspect(cmd []string) { + if len(cmd) == 2 { + regexIsNumeric := regexp.MustCompile(`^\d+$`) + if regexIsNumeric.MatchString(cmd[1]) { + // Then it must be a message id + id, _ := strconv.Atoi(cmd[1]) + go printMessage(id) + + } else { + go printUser(strings.ReplaceAll(cmd[1], "@", "")) + } + + } else { + printInfo(fmt.Sprintf("To inspect something use %sid ", config.Basics.CmdPrefix)) + } + +} +func printMessage(id int) { + chat := k.NewChat(channel) + messages, err := chat.ReadMessage(id) + if err == nil { + var response StyledString + if messages != nil && len((*messages).Result.Messages) > 0 { + message := (*messages).Result.Messages[0].Msg + var apiCast keybase.ChatAPI + apiCast.Msg = &message + response = formatOutput(apiCast) + } else { + response = config.Colors.Feed.Error.stylize("message not found") + } + printToView("Chat", response.string()) + } +} + +func formatProofs(userLookup keybase.UserAPI) StyledString { + messageColor := config.Colors.Message + message := basicStyle.stylize("") + for _, proof := range userLookup.Them[0].ProofsSummary.All { + style := config.Colors.Feed.Success + if proof.State != 1 { + style = config.Colors.Feed.Error + } + proofString := style.stylize("Proof [$NAME@$SITE]: $URL\n") + proofString = proofString.replace("$NAME", messageColor.SenderDefault.stylize(proof.Nametag)) + proofString = proofString.replace("$SITE", messageColor.SenderDevice.stylize(proof.ProofType)) + proofString = proofString.replace("$URL", messageColor.LinkURL.stylize(proof.HumanURL)) + message = message.append(proofString) + } + return message.appendString("\n") +} +func formatProfile(userLookup keybase.UserAPI) StyledString { + messageColor := config.Colors.Message + user := userLookup.Them[0] + profileText := messageColor.Body.stylize("Name: $FNAME\nLocation: $LOC\nBio: $BIO\n") + profileText = profileText.replaceString("$FNAME", user.Profile.FullName) + profileText = profileText.replaceString("$LOC", user.Profile.Location) + profileText = profileText.replaceString("$BIO", user.Profile.Bio) + + return profileText +} + +func formatFollowState(userLookup keybase.UserAPI) StyledString { + username := userLookup.Them[0].Basics.Username + followSteps := followedInSteps[username] + if followSteps == 1 { + return config.Colors.Feed.Success.stylize("\n\n") + } else if followSteps > 1 { + var steps []string + for head := username; head != ""; head = trustTreeParent[head] { + steps = append(steps, fmt.Sprintf("[%s]", head)) + } + trustLine := fmt.Sprintf("Indirect follow: <%s>\n\n", strings.Join(steps, " Followed by ")) + return config.Colors.Message.Body.stylize(trustLine) + } + + return basicStyle.stylize("") + +} + +func formatFollowerAndFollowedList(username string, listType string) StyledString { + messageColor := config.Colors.Message + response := basicStyle.stylize("") + bytes, _ := k.Exec("list-"+listType, username) + bigString := string(bytes) + lines := strings.Split(bigString, "\n") + response = response.appendString(fmt.Sprintf("%s (%d): ", listType, len(lines)-1)) + for i, user := range lines[:len(lines)-1] { + if i != 0 { + response = response.appendString(", ") + } + response = response.append(messageColor.LinkKeybase.stylize(user)) + response = response.append(getUserFlags(user)) + } + return response.appendString("\n\n") +} + +func printUser(username string) { + messageColor := config.Colors.Message + + userLookup, _ := k.UserLookup(username) + + response := messageColor.Header.stylize("[Inspecting `$USER`]\n") + response = response.replace("$USER", messageColor.SenderDefault.stylize(username)) + response = response.append(formatProfile(userLookup)) + response = response.append(formatFollowState(userLookup)) + + response = response.append(formatProofs(userLookup)) + response = response.append(formatFollowerAndFollowedList(username, "followers")) + response = response.append(formatFollowerAndFollowedList(username, "following")) + + printToView("Chat", response.string()) +} diff --git a/cmdUnfollow.go b/cmdUnfollow.go new file mode 100644 index 0000000..241e545 --- /dev/null +++ b/cmdUnfollow.go @@ -0,0 +1,33 @@ +// +build !rm_basic_commands allcommands followcmd + +package main + +import ( + "fmt" +) + +func init() { + command := Command{ + Cmd: []string{"unfollow"}, + Description: "$username - Unfollows the given user", + Help: "", + Exec: cmdUnfollow, + } + RegisterCommand(command) +} + +func cmdUnfollow(cmd []string) { + if len(cmd) == 2 { + go unfollow(cmd[1]) + } else { + printUnfollowHelp() + } +} +func unfollow(username string) { + k.Exec("unfollow", username) + printInfoF("Now unfollows $TEXT", config.Colors.Message.LinkKeybase.stylize(username)) +} + +func printUnfollowHelp() { + printInfo(fmt.Sprintf("To unfollow a user use %sunfollow ", config.Basics.CmdPrefix)) +} diff --git a/cmdWall.go b/cmdWall.go index 51f27b4..168729b 100644 --- a/cmdWall.go +++ b/cmdWall.go @@ -81,7 +81,7 @@ func cmdPopulateWall(cmd []string) { apiCast.Msg = &api.Result.Messages[i].Msg result[apiCast.Msg.SentAt] = apiCast newMessage := formatOutput(apiCast) - printMe = append(printMe, newMessage) + printMe = append(printMe, newMessage.string()) } } @@ -96,7 +96,7 @@ func cmdPopulateWall(cmd []string) { sort.Ints(keys) time.Sleep(1 * time.Millisecond) for _, k := range keys { - actuallyPrintMe += formatOutput(result[k]) + "\n" + actuallyPrintMe += formatOutput(result[k]).string() + "\n" } printToView("Chat", fmt.Sprintf("\n\n\n%s\nYour wall query took %s\n\n", actuallyPrintMe, time.Since(start))) } diff --git a/defaultConfig.go b/defaultConfig.go index 0471703..f017e9d 100644 --- a/defaultConfig.go +++ b/defaultConfig.go @@ -11,9 +11,9 @@ cmd_prefix = "/" [formatting] # BASH-like PS1 variable equivalent -output_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" -output_stream_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" -output_mention_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" +output_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG" +output_stream_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG" +output_mention_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG" pm_format = "PM from $USER@$DEVICE: $MSG" # 02 = Day, Jan = Month, 06 = Year @@ -22,6 +22,8 @@ date_format = "02Jan06" # 15 = hours, 04 = minutes, 05 = seconds time_format = "15:04" +icon_following_user = "[*]" +icon_indirect_following_user = "[?]" [colors] [colors.channels] @@ -34,44 +36,52 @@ time_format = "15:04" foreground = "green" italic = true - [colors.message] - [colors.message.body] - foreground = "normal" - [colors.message.header] - foreground = "grey" - bold = true - [colors.message.mention] - foreground = "green" - italic = true - bold = true - [colors.message.id] - foreground = "yellow" - [colors.message.time] - foreground = "magenta" - [colors.message.sender_default] - foreground = "cyan" - bold = true - [colors.message.sender_device] - foreground = "cyan" - [colors.message.attachment] - foreground = "red" - [colors.message.link_url] - foreground = "yellow" - [colors.message.link_keybase] - foreground = "yellow" - [colors.message.reaction] - foreground = "magenta" - bold = true + [colors.message] + [colors.message.body] + foreground = "normal" + [colors.message.header] + foreground = "grey" + bold = true + [colors.message.mention] + foreground = "green" + italic = true + bold = true + [colors.message.id] + foreground = "yellow" + bold = true + [colors.message.time] + foreground = "magenta" + bold = true + [colors.message.sender_default] + foreground = "cyan" + bold = true + [colors.message.sender_device] + foreground = "cyan" + bold = true + [colors.message.sender_tags] + foreground = "yellow" + [colors.message.attachment] + foreground = "red" + [colors.message.link_url] + foreground = "yellow" + [colors.message.link_keybase] + foreground = "cyan" + [colors.message.reaction] + foreground = "magenta" + bold = true [colors.message.quote] foreground = "green" - [colors.message.code] - foreground = "green" - background = "grey" - [colors.feed] - [colors.feed.basic] - foreground = "grey" - [colors.feed.error] - foreground = "red" + [colors.message.code] + foreground = "cyan" + background = "grey" + + [colors.feed] + [colors.feed.basic] + foreground = "grey" + [colors.feed.error] + foreground = "red" + [colors.feed.success] + foreground = "green" [colors.feed.file] foreground = "yellow" ` diff --git a/kbtui.toml b/kbtui.toml index fa98691..871de8f 100644 --- a/kbtui.toml +++ b/kbtui.toml @@ -8,9 +8,9 @@ cmd_prefix = "/" [formatting] # BASH-like PS1 variable equivalent -output_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" -output_stream_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" -output_mention_format = "┌──[$USER@$DEVICE] [$ID] [$DATE - $TIME]\n└╼ $MSG" +output_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG" +output_stream_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG" +output_mention_format = "┌──[$USER@$DEVICE$TAGS] [$ID] [$DATE - $TIME]\n└╼ $MSG" pm_format = "PM from $USER@$DEVICE: $MSG" # 02 = Day, Jan = Month, 06 = Year @@ -19,6 +19,8 @@ date_format = "02Jan06" # 15 = hours, 04 = minutes, 05 = seconds time_format = "15:04" +icon_following_user = "[*]" +icon_indirect_following_user = "[?]" [colors] [colors.channels] @@ -31,43 +33,51 @@ time_format = "15:04" foreground = "green" italic = true - [colors.message] - [colors.message.body] - foreground = "normal" - [colors.message.header] - foreground = "grey" - bold = true - [colors.message.mention] - foreground = "green" - italic = true - bold = true - [colors.message.id] - foreground = "yellow" - [colors.message.time] - foreground = "magenta" - [colors.message.sender_default] - foreground = "cyan" - bold = true - [colors.message.sender_device] - foreground = "cyan" - [colors.message.attachment] - foreground = "red" - [colors.message.link_url] - foreground = "yellow" - [colors.message.link_keybase] - foreground = "yellow" - [colors.message.reaction] - foreground = "magenta" - bold = true + [colors.message] + [colors.message.body] + foreground = "normal" + [colors.message.header] + foreground = "grey" + bold = true + [colors.message.mention] + foreground = "green" + italic = true + bold = true + [colors.message.id] + foreground = "yellow" + bold = true + [colors.message.time] + foreground = "magenta" + bold = true + [colors.message.sender_default] + foreground = "cyan" + bold = true + [colors.message.sender_device] + foreground = "cyan" + bold = true + [colors.message.sender_tags] + foreground = "yellow" + [colors.message.attachment] + foreground = "red" + [colors.message.link_url] + foreground = "yellow" + [colors.message.link_keybase] + foreground = "cyan" + [colors.message.reaction] + foreground = "magenta" + bold = true [colors.message.quote] foreground = "green" - [colors.message.code] - foreground = "green" - background = "grey" - [colors.feed] - [colors.feed.basic] - foreground = "grey" - [colors.feed.error] - foreground = "red" + [colors.message.code] + foreground = "cyan" + background = "grey" + + [colors.feed] + [colors.feed.basic] + foreground = "grey" + [colors.feed.error] + foreground = "red" + [colors.feed.success] + foreground = "green" [colors.feed.file] foreground = "yellow" diff --git a/main.go b/main.go index 67f2a06..16fa539 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,8 @@ func main() { RunCommand(os.Args...) } + // Create map of users following users, to populate flags + go generateFollowersList() fmt.Println("initKeybindings") if err := initKeybindings(); err != nil { fmt.Printf("%+v", err) @@ -405,7 +407,7 @@ func populateChat() { } var apiCast keybase.ChatAPI apiCast.Msg = &message.Msg - newMessage := formatOutput(apiCast) + newMessage := formatOutput(apiCast).string() printMe = append(printMe, newMessage) } } @@ -424,6 +426,7 @@ func populateList() { log.Printf("%+v", err) } else { clearView("List") + var textBase = config.Colors.Channels.Basic.stylize("") var recentPMs = textBase.append(config.Colors.Channels.Header.stylize("---[PMs]---\n")) var recentPMsCount = 0 @@ -465,10 +468,10 @@ func populateList() { func formatMessageBody(body string) StyledString { message := config.Colors.Message.Body.stylize(body) + message = message.colorRegex(`@[\w_]*([\.#][\w_]+)*`, config.Colors.Message.LinkKeybase) message = colorReplaceMentionMe(message) message = message.colorRegex(`_[^_]*_`, config.Colors.Message.Body.withItalic()) message = message.colorRegex(`~[^~]*~`, config.Colors.Message.Body.withStrikethrough()) - message = message.colorRegex(`@[\w_]*([\.#][\w_]+)*`, config.Colors.Message.LinkKeybase) // TODO change how bold, italic etc works, so it uses boldOn boldOff ([1m and [22m) message = message.colorRegex(`\*[^\*]*\*`, config.Colors.Message.Body.withBold()) message = message.colorRegex(">.*$", config.Colors.Message.Quote) @@ -485,6 +488,7 @@ func formatMessageBody(body string) StyledString { } output += line + strings.Repeat(" ", spaces) + "\n" } + // TODO stylize should remove formatting - in general everything should return config.Colors.Message.Code.stylize(output).stringFollowedByStyle(message.style) }) message = message.colorRegex("`[^`]*`", config.Colors.Message.Code) @@ -515,43 +519,46 @@ func cleanChannelName(c string) string { } func formatMessage(api keybase.ChatAPI, formatString string) StyledString { + msg := api.Msg ret := config.Colors.Message.Header.stylize("") - msgType := api.Msg.Content.Type + msgType := msg.Content.Type switch msgType { case "text", "attachment": ret = config.Colors.Message.Header.stylize(formatString) - tm := time.Unix(int64(api.Msg.SentAt), 0) - var msg = formatMessageBody(api.Msg.Content.Text.Body) + tm := time.Unix(int64(msg.SentAt), 0) + var body = formatMessageBody(msg.Content.Text.Body) if msgType == "attachment" { - msg = config.Colors.Message.Body.stylize("$TITLE\n$FILE") - attachment := api.Msg.Content.Attachment - msg = msg.replaceString("$TITLE", attachment.Object.Title) - msg = msg.replace("$FILE", config.Colors.Message.Attachment.stylize(fmt.Sprintf("[Attachment: %s]", attachment.Object.Filename))) + body = config.Colors.Message.Body.stylize("$TITLE\n$FILE") + attachment := msg.Content.Attachment + body = body.replaceString("$TITLE", attachment.Object.Title) + body = body.replace("$FILE", config.Colors.Message.Attachment.stylize(fmt.Sprintf("[Attachment: %s]", attachment.Object.Filename))) } - user := colorUsername(api.Msg.Sender.Username) - device := config.Colors.Message.SenderDevice.stylize(api.Msg.Sender.DeviceName) - msgID := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", api.Msg.ID)) + user := colorUsername(msg.Sender.Username) + device := config.Colors.Message.SenderDevice.stylize(msg.Sender.DeviceName) + msgID := config.Colors.Message.ID.stylize(fmt.Sprintf("%d", msg.ID)) date := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.DateFormat)) msgTime := config.Colors.Message.Time.stylize(tm.Format(config.Formatting.TimeFormat)) - channelName := config.Colors.Message.ID.stylize(fmt.Sprintf("@%s#%s", api.Msg.Channel.Name, api.Msg.Channel.TopicName)) - ret = ret.replace("$MSG", msg) + channelName := config.Colors.Message.ID.stylize(fmt.Sprintf("@%s#%s", msg.Channel.Name, msg.Channel.TopicName)) + ret = ret.replace("$MSG", body) ret = ret.replace("$USER", user) ret = ret.replace("$DEVICE", device) ret = ret.replace("$ID", msgID) ret = ret.replace("$TIME", msgTime) ret = ret.replace("$DATE", date) ret = ret.replace("$TEAM", channelName) + ret = ret.replace("$TAGS", getUserFlags(api.Msg.Sender.Username)) } return ret } -func formatOutput(api keybase.ChatAPI) string { + +func formatOutput(api keybase.ChatAPI) StyledString { format := config.Formatting.OutputFormat if stream { format = config.Formatting.OutputStreamFormat } - return formatMessage(api, format).string() + return formatMessage(api, format) } // End formatting @@ -594,7 +601,7 @@ func handleMessage(api keybase.ChatAPI) { } if api.Msg.Channel.MembersType == channel.MembersType && cleanChannelName(api.Msg.Channel.Name) == channel.Name { if channel.MembersType == keybase.USER || channel.MembersType == keybase.TEAM && channel.TopicName == api.Msg.Channel.TopicName { - printToView("Chat", formatOutput(api)) + printToView("Chat", formatOutput(api).string()) chat := k.NewChat(channel) lastMessage.ID = api.Msg.ID chat.Read(api.Msg.ID) @@ -602,7 +609,7 @@ func handleMessage(api keybase.ChatAPI) { } } else { if api.Msg.Channel.MembersType == keybase.TEAM { - printToView("Chat", formatOutput(api)) + printToView("Chat", formatOutput(api).string()) } else { printToView("Chat", formatMessage(api, config.Formatting.PMFormat).string()) } diff --git a/types.go b/types.go index 1d68216..676ef97 100644 --- a/types.go +++ b/types.go @@ -36,12 +36,14 @@ type Basics struct { // Formatting holds the 'formatting' section of the config file type Formatting struct { - OutputFormat string `toml:"output_format"` - OutputStreamFormat string `toml:"output_stream_format"` - OutputMentionFormat string `toml:"output_mention_format"` - PMFormat string `toml:"pm_format"` - DateFormat string `toml:"date_format"` - TimeFormat string `toml:"time_format"` + OutputFormat string `toml:"output_format"` + OutputStreamFormat string `toml:"output_stream_format"` + OutputMentionFormat string `toml:"output_mention_format"` + PMFormat string `toml:"pm_format"` + DateFormat string `toml:"date_format"` + TimeFormat string `toml:"time_format"` + IconFollowingUser string `toml:"icon_following_user"` + IconIndirectFollowUser string `toml:"icon_indirect_following_user"` } // Colors holds the 'colors' section of the config file @@ -75,9 +77,11 @@ type Message struct { Header Style `toml:"header"` Mention Style `toml:"mention"` ID Style `toml:"id"` + Tags Style `toml:"tags"` Time Style `toml:"time"` SenderDefault Style `toml:"sender_default"` SenderDevice Style `toml:"sender_device"` + SenderTags Style `toml:"sender_tags"` Attachment Style `toml:"attachment"` LinkURL Style `toml:"link_url"` LinkKeybase Style `toml:"link_keybase"` @@ -88,7 +92,8 @@ type Message struct { // Feed holds the style information for various elements of the feed window type Feed struct { - Basic Style `toml:"basic"` - Error Style `toml:"error"` - File Style `toml:"file"` + Basic Style `toml:"basic"` + Error Style `toml:"error"` + File Style `toml:"file"` + Success Style `toml:"success"` } diff --git a/userTags.go b/userTags.go new file mode 100644 index 0000000..cf2dca0 --- /dev/null +++ b/userTags.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "strings" +) + +var followedInSteps = make(map[string]int) +var trustTreeParent = make(map[string]string) + +func clearFlagCache() { + followedInSteps = make(map[string]int) + trustTreeParent = make(map[string]string) +} + +var maxDepth = 4 + +func generateFollowersList() { + // Does a BFS of followedInSteps + queue := []string{k.Username} + printInfo("Generating Tree of Trust...") + lastDepth := 1 + for len(queue) > 0 { + head := queue[0] + queue = queue[1:] + depth := followedInSteps[head] + 1 + if depth > maxDepth { + continue + } + if depth > lastDepth { + printInfo(fmt.Sprintf("Trust generated at Level #%d", depth-1)) + lastDepth = depth + } + + bytes, _ := k.Exec("list-following", head) + bigString := string(bytes) + following := strings.Split(bigString, "\n") + for _, user := range following { + if followedInSteps[user] == 0 && user != k.Username { + followedInSteps[user] = depth + trustTreeParent[user] = head + queue = append(queue, user) + } + } + } + printInfo(fmt.Sprintf("Trust-level estabilished for %d users", len(followedInSteps))) +} + +func getUserFlags(username string) StyledString { + tags := "" + followDepth := followedInSteps[username] + if followDepth == 1 { + tags += fmt.Sprintf(" %s", config.Formatting.IconFollowingUser) + } else if followDepth > 1 { + tags += fmt.Sprintf(" %s%d", config.Formatting.IconIndirectFollowUser, followDepth-1) + } + return config.Colors.Message.SenderTags.stylize(tags) +}