Skip to content

Commit

Permalink
Text display of conversations
Browse files Browse the repository at this point in the history
  • Loading branch information
f-ewald committed May 27, 2022
1 parent e1a07ea commit 417dbfd
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 50 deletions.
79 changes: 67 additions & 12 deletions cmd/conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
hermes "github.com/f-ewald/hermes/pkg"
"github.com/spf13/cobra"
"log"
"strconv"
)

func init() {
rootCmd.AddCommand(conversationCmd)
conversationCmd.AddCommand(conversationGetCmd)
conversationCmd.AddCommand(conversationListCmd)

// Here you will define your flags and configuration settings.
Expand All @@ -24,19 +26,66 @@ func init() {

// conversationCmd represents the conversation command.
var conversationCmd = &cobra.Command{
Use: "conversation",
Short: "Show conversations, find participants",
Use: "conversation",
Short: "Show retrieveConversations, find participants",
Aliases: []string{"retrieveConversations"},
//Long: `TODO`,
//Run: func(cmd *cobra.Command, args []string) {
// fmt.Println("conversation called")
//},
}

func retrieveConversations() []*hermes.Chat {
messageDB, err := hermes.MessageDBFilename()
if err != nil {
log.Fatal(err)
}
db := hermes.NewDatabase(messageDB)
err = db.Connect()
if err != nil {
log.Fatal(err)
}
defer func() {
err = db.Close()
if err != nil {
log.Fatal(err)
}
}()

handles, err := db.ListConversations(context.Background())
if err != nil {
log.Fatal(err)
}
return handles
}

var conversationListCmd = &cobra.Command{
Use: "list",
Short: "List all conversations",
Long: "List all conversations with ",
Short: "List all retrieveConversations",
Long: "List all retrieveConversations",
Run: func(cmd *cobra.Command, args []string) {
chats := retrieveConversations()

output, err := cfg.Formatter.Format(chats, "conversation-list.tpl")
if err != nil {
panic(err)
}
var printer hermes.Printer
printer = &hermes.StdoutPrinter{}
printer.Print(output)
},
}

var conversationGetCmd = &cobra.Command{
Use: "get NUMBER ... ",
Short: "Get conversation",
Long: "Get conversation",
Args: cobra.MinimumNArgs(1),
ValidArgs: []string{"NUMBER"},
Run: func(cmd *cobra.Command, args []string) {
var printer hermes.Printer
printer = &hermes.StdoutPrinter{}

messageDB, err := hermes.MessageDBFilename()
if err != nil {
log.Fatal(err)
Expand All @@ -53,14 +102,20 @@ var conversationListCmd = &cobra.Command{
}
}()

conversations, err := db.ListConversations(context.Background())
if err != nil {
log.Fatal(err)
}
var printer hermes.Printer
printer = &hermes.StdoutPrinter{}
for _, conversation := range conversations {
printer.Print([]byte(conversation))
for _, arg := range args {
chatID, err := strconv.Atoi(arg)
if err != nil {
panic(err)
}
conversations, err := db.Conversation(context.Background(), chatID)
if err != nil {
panic(err)
}
b, err := cfg.Formatter.Format(conversations, "conversation.tpl")
if err != nil {
panic(err)
}
printer.Print(b)
}
},
}
23 changes: 20 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
hermes "github.com/f-ewald/hermes/pkg"
"github.com/spf13/cobra"
"os"

Expand All @@ -11,12 +12,16 @@ import (

var cfgFile string

var output string

// Config bundles the configuration.
type Config struct {
// Output defines the output format. If nothing is defined, normal text will be written to
// STDOUT.
Output string

Formatter hermes.Formatter

// ChatDB contains the full path to the chat database.
ChatDB string
}
Expand All @@ -29,7 +34,19 @@ var rootCmd = &cobra.Command{
Use: "hermes",
Short: "Command-line interface for iMessage databases",
Long: `Hermes is a command-line interface for iMessage databases.
You can use it to analyze and display conversations and view statistics.`,
You can use it to analyze and display retrieveConversations and view statistics.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
switch output {
case "text":
cfg.Formatter = &hermes.TextFormatter{}
case "json":
cfg.Formatter = &hermes.JsonFormatter{}
case "yaml":
cfg.Formatter = &hermes.YamlFormatter{}
default:
panic("output must be one of json, text or yaml")
}
},
}

// Execute adds all child commands to the root command and sets flags appropriately.
Expand All @@ -42,8 +59,8 @@ func init() {
cobra.OnInitialize(initConfig)

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.hermes.yaml)")
rootCmd.PersistentFlags().StringVarP(&cfg.Output, "output", "o", "text", "The output format. Can be either json, yaml or text")
rootCmd.PersistentFlags().StringVar(&cfg.ChatDB, "db", "", "Full path to the chat database if it is different than the default path.")
rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "text", "The output format. Can be either json, yaml or text")
rootCmd.PersistentFlags().StringVarP(&cfg.ChatDB, "database", "d", "", "Full path to the chat database if it is different than the default path.")
}

// initConfig reads in config file and ENV variables if set.
Expand Down
14 changes: 1 addition & 13 deletions cmd/statistics.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,6 @@ var statisticsCmd = &cobra.Command{
Long: `Analyze all messages that have been sent and received and display the statistics.
Supports text, JSON and YAML output format.`,
Run: func(cmd *cobra.Command, args []string) {
var formatter hermes.Formatter
switch cfg.Output {
case "text":
formatter = &hermes.TextFormatter{}
case "json":
formatter = &hermes.JsonFormatter{}
case "yaml":
formatter = &hermes.YamlFormatter{}
default:
panic("invalid output format")
}

messageDB, err := hermes.MessageDBFilename()
if err != nil {
log.Fatal(err)
Expand All @@ -50,7 +38,7 @@ Supports text, JSON and YAML output format.`,
if err != nil {
log.Fatal(err)
}
formatted, err := formatter.Format(stats)
formatted, err := cfg.Formatter.Format(stats, "statistics.tpl")
if err != nil {
log.Fatal(err)
}
Expand Down
148 changes: 136 additions & 12 deletions pkg/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"database/sql"
_ "github.com/mattn/go-sqlite3"
"sort"
"strings"
"time"
)

type Database struct {
Expand Down Expand Up @@ -79,35 +82,156 @@ func (db *Database) Statistics(ctx context.Context) (stats *Statistics, err erro
return stats, nil
}

type Conversation struct {
Messages []*Message `json:"messages"`
// Participant is represented by a handle in the database
type Participant struct {
ID int `json:"id" yaml:"id"`
Number string `json:"number" yaml:"number"`
Country string `json:"country" yaml:"country"`
Service string `json:"service" yaml:"service"`
}

// Chat has a unique ID and at least one participant.
type Chat struct {
ID int `json:"id" yaml:"id"`
Participants []*Participant `json:"participants,omitempty" yaml:"participants,omitempty"`
Messages []*Message `json:"messages,omitempty" yaml:"messages,omitempty"`
}

type Message struct {
Text string `json:"text"`
SenderID int `json:"sender_id" yaml:"sender-id"`
Text string `json:"text" yaml:"text"`
Date time.Time `json:"date" yaml:"date"`
}

func (db *Database) Conversation(ctx context.Context) (conversation *Conversation, err error) {
// Conversation returns all messages from a conversation with the given identifier if available.
func (db *Database) Conversation(ctx context.Context, chatID int) (chat *Chat, err error) {
rows, err := db.db.QueryContext(ctx, `SELECT
datetime (message.date / 1000000000 + strftime ("%s", "2001-01-01"), "unixepoch", "localtime") AS message_date,
message.text,
message.is_from_me,
handle."ROWID",
handle.id,
handle.country,
handle.service
FROM
chat
JOIN chat_message_join ON chat. "ROWID" = chat_message_join.chat_id
JOIN message ON chat_message_join.message_id = message. "ROWID"
LEFT JOIN handle on message.handle_id = handle."ROWID"
WHERE
chat."ROWID" = ?`, chatID)
if err != nil {
return nil, err
}
defer func() {
_ = rows.Close()
}()

participantMap := make(map[int]*Participant)
chat = &Chat{
Participants: make([]*Participant, 0),
Messages: make([]*Message, 0),
}
for rows.Next() {
var dateString string
p := &Participant{}
m := &Message{}
var isFromMe bool
var text sql.NullString
var participantID sql.NullInt64
var participantNumber sql.NullString
var participantCountry sql.NullString
var participantService sql.NullString
err = rows.Scan(&dateString, &text, &isFromMe, &participantID, &participantNumber, &participantCountry,
&participantService)
if err != nil {
return nil, err
}

if participantID.Valid {
p.ID = int(participantID.Int64)
}
if participantNumber.Valid {
p.Number = participantNumber.String
}
if participantCountry.Valid {
p.Country = participantCountry.String
}
if participantService.Valid {
p.Service = participantService.String
}

return nil, nil
// Copy sender ID to message
m.SenderID = p.ID

if text.Valid {
m.Text = text.String
}
m.Date, err = time.Parse("2006-01-02 15:04:05", dateString)
if err != nil {
panic(err)
}

chat.Messages = append(chat.Messages, m)

if p.ID != 0 {
if _, ok := participantMap[p.ID]; !ok {
participantMap[p.ID] = p
}
}
}

for _, p := range participantMap {
chat.Participants = append(chat.Participants, p)
}

return chat, nil
}

func (db *Database) ListConversations(ctx context.Context) (conversations []string, err error) {
rows, err := db.db.QueryContext(ctx, "SELECT guid FROM chat")
func (db *Database) ListConversations(ctx context.Context) (chats []*Chat, err error) {
rows, err := db.db.QueryContext(ctx, `SELECT
chat."ROWID", handle."ROWID", handle.id, handle.country, handle.service
FROM
chat
JOIN chat_handle_join ON chat."ROWID" = chat_handle_join.chat_id
JOIN handle ON chat_handle_join.handle_id = handle."ROWID"`)
if err != nil {
return nil, err
}
defer func() {
_ = rows.Close()
}()
conversations = make([]string, 0)
chatMap := make(map[int]*Chat)
for rows.Next() {
var guid string
err = rows.Scan(&guid)
var chatID int
var p Participant
err = rows.Scan(&chatID, &p.ID, &p.Number, &p.Country, &p.Service)
if err != nil {
return nil, err
}
conversations = append(conversations, guid)

// Normalize values
p.Service = strings.ToUpper(p.Service)
p.Country = strings.ToUpper(p.Country)

if _, ok := chatMap[chatID]; !ok {
chatMap[chatID] = &Chat{
ID: chatID,
Participants: make([]*Participant, 0),
}
}
chatMap[chatID].Participants = append(chatMap[chatID].Participants, &p)
}
return conversations, nil

chats = make([]*Chat, 0)
for _, chat := range chatMap {
chats = append(chats, chat)
}

// Sort chats by ID so that they are always shown in the same order.
sort.Slice(chats, func(i, j int) bool {
return chats[i].ID < chats[j].ID
})

return chats, nil
}
Loading

0 comments on commit 417dbfd

Please sign in to comment.