Last active
August 1, 2022 11:58
-
-
Save Code-Hex/e1f78e63ebe71ffc1d7b20453c088fab to your computer and use it in GitHub Desktop.
Slack to discord message migration tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module github.com/Code-Hex/slack2discord | |
go 1.18 | |
require ( | |
github.com/Code-Hex/dd v1.1.0 // indirect | |
github.com/bwmarrin/discordgo v0.25.0 // indirect | |
github.com/gorilla/websocket v1.4.2 // indirect | |
github.com/slack-go/slack v0.11.2 // indirect | |
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect | |
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"fmt" | |
"html" | |
"os" | |
"regexp" | |
"strconv" | |
"strings" | |
"sync" | |
"time" | |
"github.com/bwmarrin/discordgo" | |
"github.com/slack-go/slack" | |
) | |
func main() { | |
if err := run(context.Background()); err != nil { | |
fmt.Fprintf(os.Stderr, "failed to run: %q", err) | |
os.Exit(1) | |
} | |
} | |
const slackChannel = "C..." // slack channel ID | |
const discordChannel = "..." // discord channel ID | |
// < # opening angle bracket | |
// ([^>\|]+) # link | |
// (?:\|([^>]+))? # label | |
// > # closing angle bracket | |
var re = regexp.MustCompile(`<([^>\|]+)(?:\|([^>]+))?>`) | |
func run(ctx context.Context) error { | |
slackClient := slack.New("xoxp-...") | |
session, err := discordgo.New("...") | |
if err != nil { | |
return err | |
} | |
messages, err := history(ctx, slackClient) | |
if err != nil { | |
return err | |
} | |
for _, m := range messages { | |
fmt.Println("-------", m.Timestamp) | |
fmt.Println(m.Text) | |
_, err = session.ChannelMessageSend(discordChannel, m.Text) | |
if err != nil { | |
return fmt.Errorf("failed to send message: %w", err) | |
} | |
time.Sleep(time.Millisecond * 500) | |
} | |
// fmt.Println(dd.Dump(messages)) | |
return nil | |
} | |
type Message struct { | |
Text string | |
Timestamp string // to use save point | |
} | |
func history(ctx context.Context, slackClient *slack.Client) ([]*Message, error) { | |
var ( | |
cursor string | |
messages []*Message | |
) | |
for i := 0; ; i++ { | |
resp, err := slackClient.GetConversationHistoryContext(ctx, &slack.GetConversationHistoryParameters{ | |
ChannelID: slackChannel, | |
Cursor: cursor, | |
}) | |
if err != nil { | |
return nil, err | |
} | |
for _, message := range resp.Messages { | |
// If failed, you can continue to specify the timestamp here! | |
// if message.Timestamp <= "1648392833.619319" { | |
// continue | |
// } | |
text := strings.TrimSpace(message.Text) | |
if text == "" { | |
continue | |
} | |
if strings.Contains(text, "さんがチャンネルに参加しました") { | |
continue | |
} | |
if strings.Contains(text, "がこのチャンネルの説明を") { | |
continue | |
} | |
replacedText := re.ReplaceAllStringFunc(text, func(s string) string { | |
parts := re.FindStringSubmatch(s) | |
link := parts[1] | |
return link | |
}) | |
t, err := ts2time(message.Timestamp) | |
if err != nil { | |
return nil, err | |
} | |
content := fmt.Sprintf("(%s)\n\n%s", t.Format("2006-01-02 15:04:05"), html.UnescapeString(replacedText)) | |
messages = append(messages, &Message{ | |
Text: content, | |
Timestamp: message.Timestamp, | |
}) | |
} | |
if !resp.HasMore { | |
break | |
} | |
cursor = resp.ResponseMetaData.NextCursor | |
time.Sleep(time.Millisecond * 500) | |
fmt.Println(i, len(messages), cursor) | |
} | |
return reverse(messages), nil | |
} | |
var jst *time.Location | |
var once sync.Once | |
func getJST() *time.Location { | |
once.Do(func() { | |
jst, _ = time.LoadLocation("Asia/Tokyo") | |
}) | |
return jst | |
} | |
func ts2time(ts string) (time.Time, error) { | |
sep := strings.Split(ts, ".") | |
if len(sep) != 2 { | |
return time.Time{}, fmt.Errorf("unexpected ts: %s", ts) | |
} | |
sec, ms := sep[0], sep[1] | |
seci, err := strconv.Atoi(sec) | |
if err != nil { | |
return time.Time{}, fmt.Errorf("seci: %w", err) | |
} | |
msi, err := strconv.Atoi(ms) | |
if err != nil { | |
return time.Time{}, fmt.Errorf("msi: %w", err) | |
} | |
return time.Unix(int64(seci), int64(msi)).In(getJST()), nil | |
} | |
func reverse(s []*Message) []*Message { | |
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { | |
s[i], s[j] = s[j], s[i] | |
} | |
return s | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Get Discord API Token for yours!
Discord の自分のアカウントの Token を取得しよう!
Get Slack API for yours!
You need to get user (
xoxp-
) token.User Token Scopes
Scopes that access user data and act on behalf of users that authorize them.